diff --git a/src/components/pages/ToolsPage.tsx b/src/components/pages/ToolsPage.tsx
index 8dd3ddc..f0b819c 100644
--- a/src/components/pages/ToolsPage.tsx
+++ b/src/components/pages/ToolsPage.tsx
@@ -108,6 +108,7 @@ export function ToolsPage() {
const [benchNoCooldown, setBenchNoCooldown] = useState(store.get('benchNoCooldown'))
const [hideTFT, setHideTFT] = useState(store.get('hideTFT'))
const [hideRightNavText, setHideRightNavText] = useState(store.get('hideRightNavText'))
+ const [fixLcuWindow, setFixLcuWindow] = useState(store.get('fixLcuWindow'))
const [windowEffect, setWindowEffect] = useState(store.get('windowEffect'))
const [champSelectAssist, setChampSelectAssist] = useState(store.get('champSelectAssist'))
const [opggBuildRecommendation, setOpggBuildRecommendation] = useState(store.get('opggBuildRecommendation'))
@@ -163,6 +164,7 @@ export function ToolsPage() {
store.onChange('unlockChromas', setUnlockChromas),
store.onChange('benchNoCooldown', setBenchNoCooldown),
store.onChange('hideTFT', setHideTFT),
+ store.onChange('fixLcuWindow', setFixLcuWindow),
store.onChange('windowEffect', setWindowEffect),
store.onChange('champSelectAssist', setChampSelectAssist),
store.onChange('opggBuildRecommendation', setOpggBuildRecommendation),
@@ -644,6 +646,15 @@ export function ToolsPage() {
onChange={(v) => { setHideRightNavText(v); store.set('hideRightNavText', v) }}
/>
+
+ { setFixLcuWindow(v); store.set('fixLcuWindow', v) }}
+ />
+
= 99 ? 'Perfect' : s.kdaNum.toFixed(2)
- const rating = getRating(s.winRate, s.kdaNum)
+ for (const s of stats) {
+ const floor = `${s.floor}楼`
+ if (s.winRate == null) {
+ chatLines.push(`${floor}: 🆕 萌新上线 (无战绩)`)
+ continue
+ }
+ const winRate = s.winRate.toFixed(1)
+ const kdaStr = s.kdaNum >= 99 ? 'Perfect' : s.kdaNum.toFixed(2)
+ const rating = getRating(s.winRate, s.kdaNum)
+ chatLines.push(`${floor}: ${rating} | 胜率${winRate}% | KDA ${kdaStr}`)
+ }
- logger.info(
- '│ %s — %s#%s — 近%d场 胜率: %s%% (%d胜%d负) | KDA: %s (%.1f/%.1f/%.1f) | %s',
- floor, s.gameName, s.tagLine,
- s.total, winRate, s.wins, s.total - s.wins,
- kdaStr, s.avgK, s.avgD, s.avgA, rating,
- )
+ chatLines.push('')
+ chatLines.push('Sona助手 ♫')
- chatLines.push(`${floor}: ${rating} | 胜率${winRate}% | KDA ${kdaStr}`)
+ const msg = chatLines.join('\n')
+ const msgType = store.get('analyzeTeamPowerMsgType') || 'celebration'
+ for (let attempt = 0; attempt < 10; attempt++) {
+ try {
+ await lcu.sendChampSelectMessage(msg, msgType)
+ logger.info('队友分析已发送到聊天框 ✓ (%s)', phaseLabel)
+ break
+ } catch {
+ if (attempt < 9) {
+ await sleep(1000)
+ } else {
+ logger.warn('聊天发送失败,聊天室始终未就绪')
+ }
}
+ }
+}
- logger.info('└────────────────────')
+async function analyzeTeammates() {
+ try {
+ const session = await lcu.getChampSelectSession()
+ const { stats } = await fetchTeamStats()
- // 等待聊天室就绪后发送
- const msg = chatLines.join('\n')
- const msgType = store.get('analyzeTeamPowerMsgType') || 'celebration'
- for (let attempt = 0; attempt < 10; attempt++) {
- try {
- await lcu.sendChampSelectMessage(msg, msgType)
- logger.info('队友分析已发送到聊天框 ✓')
- break
- } catch {
- if (attempt < 9) {
- await sleep(1000)
- } else {
- logger.warn('聊天发送失败,聊天室始终未就绪')
- }
- }
+ // 缓存 stats 供换楼后重建消息(避免重复请求 SGP)
+ analyzeTeamPowerStatsByPuuid.clear()
+ for (const s of stats) {
+ if (s.puuid) analyzeTeamPowerStatsByPuuid.set(s.puuid, s)
+ if (s.summonerId) analyzeTeamPowerStatsByPuuid.set(`sid:${s.summonerId}`, s)
}
+
+ const phaseLabel = getPhaseLabel(session.timer?.phase ?? '')
+ await sendTeamStatsMessage(stats, phaseLabel)
} catch (err) {
logger.error('队友战绩分析失败:', err)
}
}
let analyzeTeamPowerUnsub: (() => void) | null = null
+let analyzeTeamPowerUpdateUnsub: (() => void) | null = null
+/** 上次发送聊天消息时的 trades 快照,换楼会导致 trades 变化从而触发重发 */
+let lastAnalyzedTradesSnapshot = ''
+/** puuid → TeammateStats 缓存,换楼后直接从缓存重建消息,避免重复请求 SGP */
+const analyzeTeamPowerStatsByPuuid = new Map()
+
+/** 根据 myTeam 顺序 + 缓存的 stats 构造聊天消息并发送 */
+async function resendAnalyzeMessageFromCache() {
+ try {
+ const session = await lcu.getChampSelectSession()
+ if (!session?.myTeam) return
+
+ const stats: TeammateStats[] = []
+ for (let i = 0; i < session.myTeam.length; i++) {
+ const player = session.myTeam[i]
+ const stat = (player.puuid ? analyzeTeamPowerStatsByPuuid.get(player.puuid) : undefined)
+ ?? (player.summonerId ? analyzeTeamPowerStatsByPuuid.get(`sid:${player.summonerId}`) : undefined)
+
+ stats.push(stat ?? {
+ floor: i + 1,
+ summonerId: player.summonerId,
+ puuid: player.puuid,
+ gameName: player.gameName,
+ tagLine: player.tagLine,
+ winRate: null,
+ wins: 0,
+ total: 0,
+ avgK: 0,
+ avgD: 0,
+ avgA: 0,
+ kdaNum: 0,
+ })
+ }
+
+ await sendTeamStatsMessage(stats, getPhaseLabel('trade_swap'))
+ } catch (err) {
+ logger.error('队友战绩分析失败:', err)
+ }
+}
+
+/** 监听 CHAMP_SELECT Update,通过 trades 变化检测换楼 */
+function onAnalyzeTeamPowerSwap(event: LCUEventMessage) {
+ if (event.eventType !== 'Update') return
+
+ const session = event.data as ChampSelectSession
+ if (!session) return
+
+ const tradesSnapshot = JSON.stringify(session.trades ?? [])
+ if (tradesSnapshot === lastAnalyzedTradesSnapshot) return
+
+ logger.info('[AnalyzeTeamPower] 检测到换楼,重新发送队友战绩分析')
+ lastAnalyzedTradesSnapshot = tradesSnapshot
+ resendAnalyzeMessageFromCache()
+}
function updateAnalyzeTeamPower(enabled: boolean) {
if (enabled && !analyzeTeamPowerUnsub) {
@@ -635,10 +706,17 @@ function updateAnalyzeTeamPower(enabled: boolean) {
analyzeTeammates()
}
})
+ analyzeTeamPowerUpdateUnsub = lcu.observe(LcuEventUri.CHAMP_SELECT, onAnalyzeTeamPowerSwap)
logger.info('Analyze team power enabled ✓')
} else if (!enabled && analyzeTeamPowerUnsub) {
analyzeTeamPowerUnsub()
analyzeTeamPowerUnsub = null
+ if (analyzeTeamPowerUpdateUnsub) {
+ analyzeTeamPowerUpdateUnsub()
+ analyzeTeamPowerUpdateUnsub = null
+ }
+ lastAnalyzedTradesSnapshot = ''
+ analyzeTeamPowerStatsByPuuid.clear()
logger.info('Analyze team power disabled')
}
}
@@ -772,6 +850,8 @@ export function initFeatures() {
updateAutoReturnToLobby(store.get('autoReturnToLobby'))
store.onChange('autoReturnToLobby', updateAutoReturnToLobby)
+ updateFixLcuWindow(store.get('fixLcuWindow'))
+ store.onChange('fixLcuWindow', updateFixLcuWindow)
store.onChange('autoReturnMode', () => {
// 模式变化时,如果功能已启用,重新注册以应用新模式
if (store.get('autoReturnToLobby')) {
diff --git a/src/lib/features/fix-lcu-window.ts b/src/lib/features/fix-lcu-window.ts
new file mode 100644
index 0000000..ad38959
--- /dev/null
+++ b/src/lib/features/fix-lcu-window.ts
@@ -0,0 +1,67 @@
+import { logger } from '@/index'
+
+// ==================== 客户端窗口异常修复 ====================
+
+let fixLcuWindowCleanup: (() => void) | null = null
+
+/** LCU 客户端标准尺寸范围 */
+const MIN_WIDTH = 1024
+const MIN_HEIGHT = 576
+const MAX_WIDTH = 1920
+const MAX_HEIGHT = 1200
+
+function clampWindowSize() {
+ const body = document.body
+ if (!body) return
+
+ const iw = window.innerWidth
+ const ih = window.innerHeight
+ const needsClamp =
+ iw > MAX_WIDTH ||
+ ih > MAX_HEIGHT ||
+ (iw > 0 && iw < MIN_WIDTH) ||
+ (ih > 0 && ih < MIN_HEIGHT)
+
+ if (needsClamp) {
+ body.style.maxWidth = `${MAX_WIDTH}px`
+ body.style.maxHeight = `${MAX_HEIGHT}px`
+ body.style.minWidth = `${MIN_WIDTH}px`
+ body.style.minHeight = `${MIN_HEIGHT}px`
+ body.style.overflow = 'auto'
+ } else {
+ body.style.maxWidth = ''
+ body.style.maxHeight = ''
+ body.style.minWidth = ''
+ body.style.minHeight = ''
+ body.style.overflow = ''
+ }
+}
+
+export function updateFixLcuWindow(enabled: boolean) {
+ if (enabled && !fixLcuWindowCleanup) {
+ let debounceTimer: number
+ const handler = () => {
+ window.clearTimeout(debounceTimer)
+ debounceTimer = window.setTimeout(clampWindowSize, 100)
+ }
+ window.addEventListener('resize', handler)
+ clampWindowSize()
+ fixLcuWindowCleanup = () => {
+ window.removeEventListener('resize', handler)
+ window.clearTimeout(debounceTimer)
+ const b = document.body
+ if (b) {
+ b.style.maxWidth = ''
+ b.style.maxHeight = ''
+ b.style.minWidth = ''
+ b.style.minHeight = ''
+ b.style.overflow = ''
+ }
+ }
+ logger.info('Fix LCU window enabled ✓')
+ } else if (!enabled && fixLcuWindowCleanup) {
+ fixLcuWindowCleanup()
+ fixLcuWindowCleanup = null
+ logger.info('Fix LCU window disabled')
+ }
+}
diff --git a/src/lib/features/global-particle.ts b/src/lib/features/global-particle.ts
index efcd79f..330a3f0 100644
--- a/src/lib/features/global-particle.ts
+++ b/src/lib/features/global-particle.ts
@@ -19,6 +19,9 @@ function getGlobalParticleHost(): HTMLElement | null {
* 必须等待客户端主视图宿主就绪后才挂载,避免首启时被 loading/iframe 层遮挡
*/
function tryInjectGlobalParticle(): boolean {
+ // 只在 LCU 主窗口挂载粒子特效,跳过子窗口(如 Wegame 对局助手、天赋选择弹窗)
+ if (window.innerWidth < 800 || window.innerHeight < 550) return true
+
const host = getGlobalParticleHost()
if (!host) return false
@@ -49,8 +52,9 @@ function tryInjectGlobalParticle(): boolean {
}> = []
const resize = () => {
- canvas.width = window.innerWidth
- canvas.height = window.innerHeight
+ // 限制 canvas 最大尺寸,防止 CEF 子窗口(如 Wegame 对局助手)被异常撑大
+ canvas.width = Math.min(window.innerWidth, 1920)
+ canvas.height = Math.min(window.innerHeight, 1200)
}
const initParticles = () => {
diff --git a/src/lib/store.ts b/src/lib/store.ts
index e1552c8..881ae3d 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -138,6 +138,8 @@ export interface SonaConfig {
autoReturnToLobby: boolean
/** 自动返回模式: queue=自动排队, lobby=仅返回房间 */
autoReturnMode: string
+ /** 修复客户端窗口异常(最小化恢复或子窗口尺寸异常时自动校正) */
+ fixLcuWindow: boolean
}
@@ -192,6 +194,7 @@ const DEFAULT_CONFIG: SonaConfig = {
gameAnalysisPopup: false,
autoReturnToLobby: false,
autoReturnMode: 'queue',
+ fixLcuWindow: false,
}