diff --git a/content-security-service/services/sensitiveWords.js b/content-security-service/services/sensitiveWords.js index e9febbc..b41ba26 100644 --- a/content-security-service/services/sensitiveWords.js +++ b/content-security-service/services/sensitiveWords.js @@ -19,6 +19,14 @@ const SENSITIVE_WORDS = { '颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义', '反动', '暴乱', '煽动颠覆', '分裂势力', '恐怖袭击', '邪教组织', '法轮', '法轮功', '台独', '藏独', '疆独', + // Political figure names and common variants (homophone / split-char evasion) + '习近平', '刁近平', '习大大', '习主席', '习总', + 'XiJinping', 'xijinping', '习近', '近平', + '李强', '王岐山', '栗战书', '汪洋', '韩正', + '李克强', '胡锦涛', '江泽民', '温家宝', '朱镕基', + '邓小平', '毛泽东', '周恩来', '刘少奇', '彭德怀', + '薄熙来', '周永康', '徐才厚', '郭伯雄', '令计划', + '孙政才', '赵乐际', '王沪宁', '丁薛祥', '蔡奇', ], pornography: [ @@ -98,10 +106,17 @@ function checkText(text) { const matchedWords = []; const categories = new Set(); const lowerText = text.toLowerCase(); + // Strip common evasion characters for split-char detection + const strippedText = text.replace(/[\s\u3000.,;:!?·…—\-_\|\\/~~`@#$%^&*+=<>()\[\]{}""''「」『』【】()〈〕\u200b\u200c\u200d\ufeff]/g, '').toLowerCase(); for (const [category, words] of Object.entries(SENSITIVE_WORDS)) { for (const word of words) { - if (lowerText.includes(word.toLowerCase())) { + const lowerWord = word.toLowerCase(); + if (lowerText.includes(lowerWord)) { + matchedWords.push(word); + categories.add(category); + } else if (word.length >= 2 && strippedText.includes(lowerWord)) { + // Split-char evasion detected matchedWords.push(word); categories.add(category); } @@ -120,7 +135,7 @@ function checkText(text) { * @returns {string} */ function getVersion() { - return '2026-05-11-v1'; + return '2026-05-12-v2'; } module.exports = { diff --git a/js/i18n/en.js b/js/i18n/en.js index 217394e..aa0edb1 100644 --- a/js/i18n/en.js +++ b/js/i18n/en.js @@ -7,7 +7,8 @@ module.exports = { // ============================================================ // Common // ============================================================ - 'common.back': '← Back', +'common.back': '← Back', + 'common.cancel': 'Cancel', 'common.joinBtn': 'Join', 'common.cannotConnect': 'Cannot connect to server', 'common.connectFailed': 'Connection failed', @@ -208,7 +209,6 @@ module.exports = { 'settings.sound': 'Sound', 'settings.music': 'Music', 'settings.vibration': 'Vibration', - 'settings.nickname': 'Display Name', 'settings.profile': 'Profile', // ============================================================ diff --git a/js/i18n/zh.js b/js/i18n/zh.js index 49ef31f..b1a2952 100644 --- a/js/i18n/zh.js +++ b/js/i18n/zh.js @@ -7,7 +7,8 @@ module.exports = { // ============================================================ // Common // ============================================================ - 'common.back': '← 返回', +'common.back': '← 返回', + 'common.cancel': '取消', 'common.joinBtn': '加入', 'common.cannotConnect': '无法连接服务器', 'common.connectFailed': '连接失败', @@ -208,7 +209,6 @@ module.exports = { 'settings.sound': '音效', 'settings.music': '音乐', 'settings.vibration': '振动', - 'settings.nickname': '显示名字', 'settings.profile': '个人资料', // ============================================================ diff --git a/js/managers/ContentSecurityManager.js b/js/managers/ContentSecurityManager.js index d842438..430cdca 100644 --- a/js/managers/ContentSecurityManager.js +++ b/js/managers/ContentSecurityManager.js @@ -52,6 +52,13 @@ const FALLBACK_WORDS = [ // Politics '颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义', '法轮', '法轮功', '台独', '藏独', '疆独', + '习近平', '刁近平', '习大大', '习主席', '习总', + 'XiJinping', 'xijinping', '习近', '近平', + '李强', '王岐山', '栗战书', '汪洋', '韩正', + '李克强', '胡锦涛', '江泽民', '温家宝', '朱镕基', + '邓小平', '毛泽东', '周恩来', '刘少奇', '彭德怀', + '薄熙来', '周永康', '徐才厚', '郭伯雄', '令计划', + '孙政才', '赵乐际', '王沪宁', '丁薛祥', '蔡奇', // Pornography '色情', '淫秽', '裸体', '卖淫', '嫖娼', '约炮', '援交', '一夜情', '黄色视频', @@ -130,13 +137,24 @@ class ContentSecurityManager { const startTime = Date.now(); const lowerContent = content.toLowerCase(); + // Strip common evasion characters (punctuation, spaces, zero-width chars) for split-char detection + const strippedContent = content.replace(/[\s\u3000.,;:!?·…—\-_\|\\/~~`@#$%^&*+=<>()\[\]{}""''「」『』【】()〈〕\u200b\u200c\u200d\ufeff]/g, '').toLowerCase(); const matchedWords = []; for (let i = 0; i < this._words.length; i++) { const word = this._words[i]; - if (word && lowerContent.includes(word.toLowerCase())) { + if (!word) continue; + const lowerWord = word.toLowerCase(); + // Direct match + if (lowerContent.includes(lowerWord)) { + matchedWords.push(word); + if (matchedWords.length >= 3) break; + continue; + } + // Split-char evasion match: check if word chars appear in order with evasion chars between + // Only for multi-char words (length >= 2) + if (word.length >= 2 && strippedContent.includes(lowerWord)) { matchedWords.push(word); - // Early exit if we find violations (performance optimization) if (matchedWords.length >= 3) break; } } diff --git a/js/scenes/ProfileScene.js b/js/scenes/ProfileScene.js index 9d68c3d..771184b 100644 --- a/js/scenes/ProfileScene.js +++ b/js/scenes/ProfileScene.js @@ -44,6 +44,7 @@ const ProfileScene = { // Current editing state _editingField: null, // Which field is being edited _inputText: '', // Current input text + _originalText: '', // Original text before editing (for change detection) _errorMessage: '', // Error message to display _isSubmitting: false, // Whether a submission is in progress _localViolation: false, // Whether local check found violation @@ -66,6 +67,7 @@ const ProfileScene = { _descriptionRect: null, _avatarRect: null, _saveBtnRect: null, + _cancelBtnRect: null, enter() { this._editingField = null; @@ -154,18 +156,15 @@ const ProfileScene = { // Description field this._renderDescriptionField(ctx); - // Error message - if (this._errorMessage) { + // Error message (only show when NOT editing to avoid overlap) + if (this._errorMessage && !this._editingField) { ctx.fillStyle = '#FF4444'; ctx.font = '12px Arial'; ctx.textAlign = 'center'; ctx.fillText(this._errorMessage, cx, SCREEN_HEIGHT - 140); } - // Save button - this._renderSaveButton(ctx); - - // Editing overlay + // Editing overlay (includes save/cancel buttons) if (this._editingField) { this._renderEditOverlay(ctx); } @@ -215,16 +214,13 @@ const ProfileScene = { } }, - // ============================================================ - // Editing - // ============================================================ - _startEditing(field, currentValue) { const csm = GameGlobal.contentSecurityManager; if (!csm || !csm.isInitialized()) return; this._editingField = field; this._inputText = currentValue || ''; + this._originalText = currentValue || ''; this._errorMessage = ''; this._localViolation = false; @@ -234,55 +230,51 @@ const ProfileScene = { if (field === FIELD.SIGNATURE) maxLength = SIGNATURE_MAX; if (field === FIELD.DESCRIPTION) maxLength = DESCRIPTION_MAX; - // Show keyboard + // Show keyboard with empty defaultValue, so the native "Done" button highlights + // as soon as user types any character. The original value is preserved in + // _originalText and shown in the overlay preview for reference. wx.showKeyboard({ - defaultValue: this._inputText, + defaultValue: '', maxLength, multiple: field === FIELD.DESCRIPTION, confirmHold: false, confirmType: 'done', }); + // Reset _inputText so we know if user actually typed something + this._inputText = ''; // Listen for keyboard input this._onKeyboardInput = (res) => { - this._inputText = res.value; + this._inputText = res.value || ''; + this._validateInput(field); + }; - // Real-time local sensitive word check - if (this._inputText) { - const localCheck = csm.checkLocalText(this._inputText); - if (localCheck.hasViolation) { - this._localViolation = true; - this._errorMessage = '内容包含违规信息,请修改'; - } else { - this._localViolation = false; - this._errorMessage = ''; - } - - // Length validation - if (field === FIELD.NICKNAME) { - if (this._inputText.length < NICKNAME_MIN) { - this._errorMessage = `昵称至少需要${NICKNAME_MIN}个字符`; - this._localViolation = true; - } else if (this._inputText.length > NICKNAME_MAX) { - this._errorMessage = `昵称不能超过${NICKNAME_MAX}个字符`; - this._localViolation = true; - } - } else if (field === FIELD.SIGNATURE && this._inputText.length > SIGNATURE_MAX) { - this._errorMessage = `签名不能超过${SIGNATURE_MAX}个字符`; - this._localViolation = true; - } else if (field === FIELD.DESCRIPTION && this._inputText.length > DESCRIPTION_MAX) { - this._errorMessage = `描述不能超过${DESCRIPTION_MAX}个字符`; - this._localViolation = true; - } + this._onKeyboardConfirm = (res) => { + // Ensure we have the final value on confirm + if (res && res.value !== undefined) { + this._inputText = res.value; + } + this._validateInput(field); + // Only auto-submit if no violations + if (!this._localViolation) { + this._handleSubmit(); } }; - this._onKeyboardConfirm = () => { - this._handleSubmit(); + this._onKeyboardComplete = (res) => { + // Keyboard closed - get final value + if (res && res.value !== undefined) { + this._inputText = res.value; + this._validateInput(field); + } }; wx.onKeyboardInput(this._onKeyboardInput); wx.onKeyboardConfirm(this._onKeyboardConfirm); + wx.onKeyboardComplete(this._onKeyboardComplete); + + // Initial validation to set correct button state + this._validateInput(field); }, _stopEditing() { @@ -294,10 +286,61 @@ const ProfileScene = { wx.offKeyboardConfirm(this._onKeyboardConfirm); this._onKeyboardConfirm = null; } + if (this._onKeyboardComplete) { + wx.offKeyboardComplete(this._onKeyboardComplete); + this._onKeyboardComplete = null; + } wx.hideKeyboard(); this._editingField = null; }, + _validateInput(field) { + const csm = GameGlobal.contentSecurityManager; + const text = this._inputText || ''; + + // Reset violation state first + this._localViolation = false; + this._errorMessage = ''; + + // Empty check + if (!text || text.trim().length === 0) { + this._localViolation = true; + this._errorMessage = '内容不能为空'; + return; + } + + // Sensitive word check + if (csm && csm.isInitialized()) { + const localCheck = csm.checkLocalText(text); + if (localCheck.hasViolation) { + this._localViolation = true; + this._errorMessage = '内容包含违规信息,请修改'; + return; + } + } + + // Length validation + if (field === FIELD.NICKNAME) { + if (text.length < NICKNAME_MIN) { + this._localViolation = true; + this._errorMessage = `昵称至少需要${NICKNAME_MIN}个字符`; + } else if (text.length > NICKNAME_MAX) { + this._localViolation = true; + this._errorMessage = `昵称不能超过${NICKNAME_MAX}个字符`; + } + } else if (field === FIELD.SIGNATURE) { + if (text.length > SIGNATURE_MAX) { + this._localViolation = true; + this._errorMessage = `签名不能超过${SIGNATURE_MAX}个字符`; + } + } else if (field === FIELD.DESCRIPTION) { + if (text.length > DESCRIPTION_MAX) { + this._localViolation = true; + this._errorMessage = `描述不能超过${DESCRIPTION_MAX}个字符`; + } + } + }, + async _handleSubmit() { if (this._isSubmitting || this._localViolation) return; @@ -478,66 +521,167 @@ const ProfileScene = { } }, - _renderSaveButton(ctx) { - const rect = this._saveBtnRect; - const isActive = this._editingField && !this._localViolation && !this._isSubmitting; - - ctx.fillStyle = isActive ? '#4a90d9' : '#555555'; - ctx.fillRect(rect.x, rect.y, rect.w, rect.h); - - ctx.fillStyle = '#FFFFFF'; - ctx.font = 'bold 14px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(this._isSubmitting ? '审核中...' : t('profile.save'), rect.x + rect.w / 2, rect.y + rect.h / 2); - }, - _renderEditOverlay(ctx) { - // Semi-transparent overlay at bottom - const overlayY = SCREEN_HEIGHT * 0.6; - ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; - ctx.fillRect(0, overlayY, SCREEN_WIDTH, SCREEN_HEIGHT - overlayY); + // Place overlay at TOP of screen, so save/cancel buttons are NOT hidden by keyboard. + // The keyboard occupies the bottom portion of the screen when shown. + const overlayY = 0; + const overlayH = Math.min(SCREEN_HEIGHT * 0.4, 220); + ctx.fillStyle = 'rgba(0, 0, 0, 0.92)'; + ctx.fillRect(0, overlayY, SCREEN_WIDTH, overlayH); - // Current input text preview - ctx.fillStyle = '#FFFFFF'; - ctx.font = '14px Arial'; + // Field label + const fieldLabels = { + [FIELD.NICKNAME]: t('profile.nickname'), + [FIELD.SIGNATURE]: t('profile.signature'), + [FIELD.DESCRIPTION]: t('profile.description'), + }; + ctx.fillStyle = '#AAAAAA'; + ctx.font = '12px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText('编辑 ' + (fieldLabels[this._editingField] || ''), CENTER_X, overlayY + 14); + // Input text preview area + const previewBoxY = overlayY + 30; + const previewBoxH = 50; + ctx.fillStyle = 'rgba(255, 255, 255, 0.08)'; + ctx.fillRect(20, previewBoxY, SCREEN_WIDTH - 40, previewBoxH); + ctx.strokeStyle = '#4a90d9'; + ctx.lineWidth = 1; + ctx.strokeRect(20, previewBoxY, SCREEN_WIDTH - 40, previewBoxH); + + // Current input text + ctx.fillStyle = '#FFFFFF'; + ctx.font = '15px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; const previewText = this._inputText || ''; - const lines = this._wrapText(ctx, previewText, INPUT_WIDTH, 3); - for (let i = 0; i < lines.length; i++) { - ctx.fillText(lines[i], CENTER_X, overlayY + 30 + i * 20); + if (previewText) { + const lines = this._wrapText(ctx, previewText, SCREEN_WIDTH - 60, 2); + for (let i = 0; i < lines.length; i++) { + ctx.fillText(lines[i], CENTER_X, previewBoxY + 16 + i * 18); + } + } else { + // Placeholder + ctx.fillStyle = '#888888'; + ctx.font = '13px Arial'; + ctx.fillText( + this._originalText ? `当前: ${this._originalText}(在下方键盘输入新内容)` : '请在下方键盘输入', + CENTER_X, + previewBoxY + previewBoxH / 2 + ); } // Character count let maxLen = 200; if (this._editingField === FIELD.NICKNAME) maxLen = NICKNAME_MAX; if (this._editingField === FIELD.SIGNATURE) maxLen = SIGNATURE_MAX; - ctx.fillStyle = '#888888'; ctx.font = '11px Arial'; - ctx.fillText(`${this._inputText.length}/${maxLen}`, CENTER_X, overlayY + 100); + ctx.textAlign = 'right'; + ctx.fillText(`${this._inputText.length}/${maxLen}`, SCREEN_WIDTH - 24, previewBoxY + previewBoxH + 12); // Violation warning - if (this._localViolation) { + if (this._localViolation && this._errorMessage) { ctx.fillStyle = '#FF4444'; ctx.font = 'bold 12px Arial'; - ctx.fillText('⚠ ' + this._errorMessage, CENTER_X, overlayY + 120); + ctx.textAlign = 'center'; + ctx.fillText('⚠ ' + this._errorMessage, CENTER_X, previewBoxY + previewBoxH + 12); } + + // Buttons row at bottom of overlay + const btnRowY = overlayY + overlayH - BTN_HEIGHT - 12; + const btnGap = 12; + const btnW = (SCREEN_WIDTH - 40 - btnGap) / 2; + + // Cancel button (left) + this._cancelBtnRect = { + x: 20, + y: btnRowY, + w: btnW, + h: BTN_HEIGHT, + }; + + // Save button (right) + this._saveBtnRect = { + x: 20 + btnW + btnGap, + y: btnRowY, + w: btnW, + h: BTN_HEIGHT, + }; + + const btnR = 6; + + // Cancel button render + ctx.fillStyle = '#3a3a3a'; + this._drawRoundRect(ctx, this._cancelBtnRect.x, this._cancelBtnRect.y, this._cancelBtnRect.w, this._cancelBtnRect.h, btnR); + ctx.fill(); + ctx.strokeStyle = '#666666'; + ctx.lineWidth = 1; + this._drawRoundRect(ctx, this._cancelBtnRect.x, this._cancelBtnRect.y, this._cancelBtnRect.w, this._cancelBtnRect.h, btnR); + ctx.stroke(); + ctx.fillStyle = '#CCCCCC'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(t('common.cancel') || '取消', this._cancelBtnRect.x + this._cancelBtnRect.w / 2, this._cancelBtnRect.y + this._cancelBtnRect.h / 2); + + // Save button render: always highlighted unless submitting + const isActive = !this._isSubmitting; + if (isActive) { + ctx.shadowColor = '#00CC66'; + ctx.shadowBlur = 16; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.fillStyle = '#00CC66'; + } else { + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + ctx.fillStyle = '#3a3a3a'; + } + this._drawRoundRect(ctx, this._saveBtnRect.x, this._saveBtnRect.y, this._saveBtnRect.w, this._saveBtnRect.h, btnR); + ctx.fill(); + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + + if (isActive) { + ctx.strokeStyle = '#33FF99'; + ctx.lineWidth = 2; + } else { + ctx.strokeStyle = '#555555'; + ctx.lineWidth = 1; + } + this._drawRoundRect(ctx, this._saveBtnRect.x, this._saveBtnRect.y, this._saveBtnRect.w, this._saveBtnRect.h, btnR); + ctx.stroke(); + + ctx.fillStyle = isActive ? '#FFFFFF' : '#666666'; + ctx.font = 'bold 15px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this._isSubmitting ? '审核中...' : t('profile.save'), this._saveBtnRect.x + this._saveBtnRect.w / 2, this._saveBtnRect.y + this._saveBtnRect.h / 2); }, _handleEditOverlayTouch(x, y) { - // Save button + // Save button - always allow click, _handleSubmit will validate if (this._hitTest(x, y, this._saveBtnRect)) { - if (!this._localViolation && !this._isSubmitting) { - this._handleSubmit(); + if (!this._isSubmitting) { + // Re-validate before submit (in case keyboard callbacks did not fire) + this._validateInput(this._editingField); + if (!this._localViolation) { + this._handleSubmit(); + } + // If validation fails, error message is now shown via _localViolation/_errorMessage } return; } - // Tap outside to cancel - this._stopEditing(); + // Cancel button + if (this._hitTest(x, y, this._cancelBtnRect)) { + this._stopEditing(); + return; + } + + // Ignore taps elsewhere inside the overlay (do NOT auto-cancel) }, // ============================================================ @@ -548,6 +692,20 @@ const ProfileScene = { return rect && x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h; }, + _drawRoundRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + }, + _wrapText(ctx, text, maxWidth, maxLines) { const words = text.split(''); const lines = []; diff --git a/js/scenes/SettingsScene.js b/js/scenes/SettingsScene.js index aee0923..4ad0688 100644 --- a/js/scenes/SettingsScene.js +++ b/js/scenes/SettingsScene.js @@ -68,7 +68,6 @@ const SettingsScene = { // Rows: nickname + 3 toggles. Distribute evenly between title and back btn. const rows = [ - { type: 'nickname' }, { type: 'toggle', key: 'soundEnabled', label: t('settings.sound'), icon: '🔊' }, { type: 'toggle', key: 'musicEnabled', label: t('settings.music'), icon: '🎵' }, { type: 'toggle', key: 'vibrationEnabled', label: t('settings.vibration'), icon: '📳' }, @@ -83,11 +82,7 @@ const SettingsScene = { for (let i = 0; i < rows.length; i++) { const row = rows[i]; const cy = firstCenterY + i * step; - if (row.type === 'nickname') { - this._renderNicknameRow(ctx, cx, cy); - } else { - this._renderToggle(ctx, cx, cy, row); - } + this._renderToggle(ctx, cx, cy, row); } // Profile entry button (below the last toggle row) @@ -98,47 +93,6 @@ const SettingsScene = { this._renderBackButton(ctx, cx, backCenterY); }, - _renderNicknameRow(ctx, cx, y) { - const w = SCREEN_WIDTH * 0.7; - const h = 50; - const x = cx - w / 2; - - this._buttons['nickname'] = { x, y: y - h / 2, w, h }; - - // Background - ctx.fillStyle = '#1e1e3a'; - ctx.fillRect(x, y - h / 2, w, h); - ctx.strokeStyle = '#333366'; - ctx.lineWidth = 1; - ctx.strokeRect(x, y - h / 2, w, h); - - // Icon + label (left) - ctx.fillStyle = COLORS.HUD_TEXT; - ctx.font = '16px Arial'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - const leftLabel = `👤 ${t('settings.nickname') || '显示名字'}`; - ctx.fillText(leftLabel, x + 15, y); - - // Current value + chevron (right) - const profile = GameGlobal.playerProfile; - let shown = ''; - if (profile) { - if (profile.granted && profile.nickname) { - shown = profile.truncate ? profile.truncate(profile.nickname, 5) : profile.nickname; - } else if (typeof profile.getDisplayName === 'function') { - const pid = (GameGlobal.networkManager && GameGlobal.networkManager.playerId) || ''; - shown = profile.getDisplayName(pid); - } - } - if (!shown) shown = 'Tanker'; - - ctx.fillStyle = profile && profile.granted ? '#FFD700' : '#8899AA'; - ctx.font = '13px Arial'; - ctx.textAlign = 'right'; - ctx.fillText(`${shown} ›`, x + w - 15, y); - }, - _renderToggle(ctx, cx, y, toggle) { const w = SCREEN_WIDTH * 0.7; const h = 50; @@ -243,10 +197,6 @@ const SettingsScene = { if (tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h) { if (key === 'back') { GameGlobal.sceneManager.switchTo(SCENE.MENU); - } else if (key === 'nickname') { - // IMPORTANT: wx.getUserProfile must be called synchronously from a - // user tap handler; invoking it here is fine (touchstart is a tap). - this._requestNicknameAuth(); } else if (key === 'profile') { const sm = GameGlobal.sceneManager; if (!sm._scenes.has(SCENE.PROFILE)) { @@ -264,68 +214,6 @@ const SettingsScene = { } }, - // ============================================================ - // Nickname acquisition (moved from MenuScene) - // ============================================================ - _requestNicknameAuth() { - const profile = GameGlobal.playerProfile; - if (!profile) return; - - const onDone = (ok) => { - if (ok) { - try { - wx.showToast({ - title: `欢迎 ${profile.nickname}`, - icon: 'none', - duration: 1500, - }); - } catch (e) { /* ignore */ } - } - }; - - if (typeof profile.requestUserProfile === 'function') { - profile.requestUserProfile().then((ok) => { - if (ok) { - onDone(true); - } else { - this._promptManualNickname(onDone); - } - }).catch(() => this._promptManualNickname(onDone)); - } else { - this._promptManualNickname(onDone); - } - }, - - _promptManualNickname(cb) { - try { - if (typeof wx === 'undefined' || typeof wx.showModal !== 'function') { - cb && cb(false); - return; - } - wx.showModal({ - title: '设置昵称', - content: '输入在对战中显示的名字(最长16字)', - editable: true, - placeholderText: '例如:坏蹄子', - confirmText: '确定', - cancelText: '取消', - success: (res) => { - if (res.confirm) { - const profile = GameGlobal.playerProfile; - const ok = profile && typeof profile.setManualNickname === 'function' - && profile.setManualNickname(res.content || ''); - cb && cb(!!ok); - } else { - cb && cb(false); - } - }, - fail: () => cb && cb(false), - }); - } catch (e) { - console.warn('[Settings] showModal failed:', e && e.message); - cb && cb(false); - } - }, }; module.exports = SettingsScene; diff --git a/server/services/sensitiveWords.js b/server/services/sensitiveWords.js index e9febbc..b41ba26 100644 --- a/server/services/sensitiveWords.js +++ b/server/services/sensitiveWords.js @@ -19,6 +19,14 @@ const SENSITIVE_WORDS = { '颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义', '反动', '暴乱', '煽动颠覆', '分裂势力', '恐怖袭击', '邪教组织', '法轮', '法轮功', '台独', '藏独', '疆独', + // Political figure names and common variants (homophone / split-char evasion) + '习近平', '刁近平', '习大大', '习主席', '习总', + 'XiJinping', 'xijinping', '习近', '近平', + '李强', '王岐山', '栗战书', '汪洋', '韩正', + '李克强', '胡锦涛', '江泽民', '温家宝', '朱镕基', + '邓小平', '毛泽东', '周恩来', '刘少奇', '彭德怀', + '薄熙来', '周永康', '徐才厚', '郭伯雄', '令计划', + '孙政才', '赵乐际', '王沪宁', '丁薛祥', '蔡奇', ], pornography: [ @@ -98,10 +106,17 @@ function checkText(text) { const matchedWords = []; const categories = new Set(); const lowerText = text.toLowerCase(); + // Strip common evasion characters for split-char detection + const strippedText = text.replace(/[\s\u3000.,;:!?·…—\-_\|\\/~~`@#$%^&*+=<>()\[\]{}""''「」『』【】()〈〕\u200b\u200c\u200d\ufeff]/g, '').toLowerCase(); for (const [category, words] of Object.entries(SENSITIVE_WORDS)) { for (const word of words) { - if (lowerText.includes(word.toLowerCase())) { + const lowerWord = word.toLowerCase(); + if (lowerText.includes(lowerWord)) { + matchedWords.push(word); + categories.add(category); + } else if (word.length >= 2 && strippedText.includes(lowerWord)) { + // Split-char evasion detected matchedWords.push(word); categories.add(category); } @@ -120,7 +135,7 @@ function checkText(text) { * @returns {string} */ function getVersion() { - return '2026-05-11-v1'; + return '2026-05-12-v2'; } module.exports = {