diff --git a/core/main_terminal.py b/core/main_terminal.py index b050e61..1651ea7 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -41,6 +41,7 @@ from modules.todo_manager import TodoManager from modules.sub_agent_manager import SubAgentManager from modules.webpage_extractor import extract_webpage_content, tavily_extract from modules.ocr_client import OCRClient +from modules.easter_egg_manager import EasterEggManager from core.tool_config import TOOL_CATEGORIES from utils.api_client import DeepSeekClient from utils.context_manager import ContextManager @@ -84,6 +85,7 @@ class MainTerminal: project_path=self.project_path, data_dir=str(self.data_dir) ) + self.easter_egg_manager = EasterEggManager() self._announced_sub_agent_tasks = set() # 聚焦文件管理 @@ -95,8 +97,12 @@ class MainTerminal: self.pending_modify_request = None # {"path": str} # 工具启用状态 - self.tool_category_states = {key: True for key in TOOL_CATEGORIES} + self.tool_category_states = { + key: category.default_enabled + for key, category in TOOL_CATEGORIES.items() + } self.disabled_tools = set() + self.disabled_notice_tools = set() self._refresh_disabled_tools() # 新增:自动开始新对话 @@ -402,7 +408,7 @@ class MainTerminal: snapshot.append({ "id": key, "label": category.label, - "enabled": self.tool_category_states.get(key, True), + "enabled": self.tool_category_states.get(key, category.default_enabled), "tools": list(category.tools), }) return snapshot @@ -410,18 +416,23 @@ class MainTerminal: def _refresh_disabled_tools(self) -> None: """刷新禁用工具列表 / Refresh disabled tool set.""" disabled = set() + notice = set() for key, enabled in self.tool_category_states.items(): if not enabled: - disabled.update(TOOL_CATEGORIES[key].tools) + category = TOOL_CATEGORIES[key] + disabled.update(category.tools) + if not getattr(category, "silent_when_disabled", False): + notice.update(category.tools) self.disabled_tools = disabled + self.disabled_notice_tools = notice def _format_disabled_tool_notice(self) -> Optional[str]: """生成禁用工具提示信息 / Format disabled tool notice.""" - if not self.disabled_tools: + if not self.disabled_notice_tools: return None lines = ["=== 工具可用性提醒 ==="] - for tool_name in sorted(self.disabled_tools): + for tool_name in sorted(self.disabled_notice_tools): lines.append(f"{tool_name}:已被用户禁用") lines.append("=== 提示结束 ===") return "\n".join(lines) @@ -1456,6 +1467,23 @@ class MainTerminal: "required": [] } } + }, + { + "type": "function", + "function": { + "name": "trigger_easter_egg", + "description": "触发隐藏彩蛋,用于展示非功能性特效。需指定 effect 参数(例如 flood 表示灌水)。", + "parameters": { + "type": "object", + "properties": { + "effect": { + "type": "string", + "description": "彩蛋标识,当前支持 flood(灌水)。" + } + }, + "required": ["effect"] + } + } } ] if self.disabled_tools: @@ -2050,6 +2078,9 @@ class MainTerminal: agent_id=arguments.get("agent_id") ) + elif tool_name == "trigger_easter_egg": + result = self.easter_egg_manager.trigger_effect(arguments.get("effect")) + else: result = {"success": False, "error": f"未知工具: {tool_name}"} diff --git a/core/tool_config.py b/core/tool_config.py index d4e87e7..0680047 100644 --- a/core/tool_config.py +++ b/core/tool_config.py @@ -9,9 +9,17 @@ from typing import Dict, List class ToolCategory: """工具类别的结构化定义。""" - def __init__(self, label: str, tools: List[str]): + def __init__( + self, + label: str, + tools: List[str], + default_enabled: bool = True, + silent_when_disabled: bool = False, + ): self.label = label self.tools = tools + self.default_enabled = default_enabled + self.silent_when_disabled = silent_when_disabled TOOL_CATEGORIES: Dict[str, ToolCategory] = { @@ -60,4 +68,10 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = { label="子智能体", tools=["create_sub_agent", "wait_sub_agent", "close_sub_agent"], ), + "easter_egg": ToolCategory( + label="彩蛋实验", + tools=["trigger_easter_egg"], + default_enabled=False, + silent_when_disabled=True, + ), } diff --git a/modules/easter_egg_manager.py b/modules/easter_egg_manager.py new file mode 100644 index 0000000..5e8a579 --- /dev/null +++ b/modules/easter_egg_manager.py @@ -0,0 +1,68 @@ +"""彩蛋触发管理器。 + +负责根据工具参数返回彩蛋元数据,供前端渲染对应特效。 +""" + +from __future__ import annotations + +from typing import Dict, List + + +class EasterEggManager: + """管理隐藏彩蛋效果的触发逻辑。""" + + def __init__(self) -> None: + # 目前仅有一个“灌水”特效,后续可在此扩展 + self.effects: Dict[str, Dict[str, object]] = { + "flood": { + "label": "灌水", + "aliases": ["flood", "water", "wave", "灌水", "注水"], + "message": "淡蓝色水面从底部缓缓上涨,并带有柔和波纹。", + "duration_seconds": 45, + "intensity_range": (0.87, 0.93), + "notes": "特效为半透明覆盖层,不会阻挡交互。", + } + } + + def trigger_effect(self, effect: str) -> Dict[str, object]: + """ + 根据传入的 effect 名称查找彩蛋。 + + Args: + effect: 彩蛋标识或别名。 + + Returns: + dict: 包含触发状态与前端所需的特效参数。 + """ + effect_key = (effect or "").strip().lower() + if not effect_key: + return self._build_error("缺少 effect 参数") + + for effect_id, metadata in self.effects.items(): + aliases = metadata.get("aliases", []) + if effect_key == effect_id or effect_key in aliases: + payload = { + "success": True, + "effect": effect_id, + "display_name": metadata.get("label", effect_id), + "message": metadata.get("message"), + "duration_seconds": metadata.get("duration_seconds", 30), + "intensity_range": metadata.get("intensity_range"), + "notes": metadata.get("notes"), + } + return payload + + return self._build_error(f"未知彩蛋: {effect_key}") + + def _build_error(self, message: str) -> Dict[str, object]: + """返回格式化的错误信息。""" + return { + "success": False, + "error": message, + "available_effects": self.available_effects, + } + + @property + def available_effects(self) -> List[str]: + """返回可用彩蛋 ID 列表。""" + return list(self.effects.keys()) diff --git a/static/app.js b/static/app.js index f653c89..883adb9 100644 --- a/static/app.js +++ b/static/app.js @@ -103,7 +103,8 @@ const TOOL_ICON_MAP = Object.freeze({ unfocus_file: 'eye', update_memory: 'brain', wait_sub_agent: 'clock', - web_search: 'search' + web_search: 'search', + trigger_easter_egg: 'sparkles' }); const TOOL_CATEGORY_ICON_MAP = Object.freeze({ @@ -114,9 +115,25 @@ const TOOL_CATEGORY_ICON_MAP = Object.freeze({ terminal_command: 'terminal', memory: 'brain', todo: 'stickyNote', - sub_agent: 'bot' + sub_agent: 'bot', + easter_egg: 'sparkles' }); +function randRange(min, max) { + return Math.random() * (max - min) + min; +} + +function randInt(min, max) { + return Math.floor(randRange(min, max + 1)); +} + +function pickOne(arr) { + if (!Array.isArray(arr) || arr.length === 0) { + return null; + } + return arr[Math.floor(Math.random() * arr.length)]; +} + function injectScriptSequentially(urls, onSuccess, onFailure) { let index = 0; const tryLoad = () => { @@ -303,6 +320,13 @@ async function bootstrapApp() { todoList: null, icons: ICONS, toolCategoryIcons: TOOL_CATEGORY_ICON_MAP, + easterEgg: { + active: false, + effect: null, + cleanupTimer: null, + styleNode: null, + retreating: false + }, // 右键菜单相关 contextMenu: { @@ -390,6 +414,8 @@ async function bootstrapApp() { clearInterval(this.subAgentPollTimer); this.subAgentPollTimer = null; } + this.destroyEasterEggEffect(); + this.finishEasterEggCleanup(); }, watch: { @@ -1075,6 +1101,9 @@ async function bootstrapApp() { if (data.result !== undefined) { targetAction.tool.result = data.result; } + if (targetAction.tool && targetAction.tool.name === 'trigger_easter_egg' && data.result !== undefined) { + this.handleEasterEggPayload(data.result); + } if (data.message !== undefined) { targetAction.tool.message = data.message; } @@ -2516,7 +2545,7 @@ async function bootstrapApp() { if (!this.quickMenuOpen) { this.quickMenuOpen = true; } - this.loadToolSettings(); + this.loadToolSettings(true); } }, @@ -3233,6 +3262,234 @@ async function bootstrapApp() { document.body.style.cursor = ''; }, + handleEasterEggPayload(payload) { + if (!payload) { + return; + } + let parsed = payload; + if (typeof payload === 'string') { + try { + parsed = JSON.parse(payload); + } catch (error) { + console.warn('无法解析彩蛋结果:', payload); + return; + } + } + if (!parsed || typeof parsed !== 'object') { + return; + } + if (!parsed.success) { + if (parsed.error) { + console.warn('彩蛋触发失败:', parsed.error); + } + this.destroyEasterEggEffect(); + return; + } + const effectName = (parsed.effect || '').toLowerCase(); + if (effectName === 'flood') { + this.startFloodEffect(parsed); + } + }, + + startFloodEffect(payload = {}) { + this.clearFloodAnimations(); + this.teardownEasterEggStyle(); + this.easterEgg.active = true; + this.easterEgg.effect = 'flood'; + this.easterEgg.retreating = false; + if (this.easterEgg.cleanupTimer) { + clearTimeout(this.easterEgg.cleanupTimer); + this.easterEgg.cleanupTimer = null; + } + this.$nextTick(() => { + this.runFloodAnimation(payload); + }); + const durationSeconds = Math.max(8, Number(payload.duration_seconds) || 45); + this.easterEgg.cleanupTimer = setTimeout(() => { + this.destroyEasterEggEffect(); + }, durationSeconds * 1000); + }, + + runFloodAnimation(payload = {}) { + const container = this.$refs.easterEggWaterContainer; + if (!container) { + return; + } + const waves = container.querySelectorAll('.wave'); + if (!waves.length) { + return; + } + const styleEl = document.createElement('style'); + styleEl.setAttribute('data-easter-egg', 'flood'); + document.head.appendChild(styleEl); + this.easterEgg.styleNode = styleEl; + const sheet = styleEl.sheet; + if (!sheet) { + return; + } + container.style.animation = 'none'; + container.style.height = '0%'; + void container.offsetHeight; + + const range = Array.isArray(payload.intensity_range) && payload.intensity_range.length === 2 + ? payload.intensity_range + : [0.85, 0.92]; + const minHeight = Math.min(range[0], range[1]); + const maxHeight = Math.max(range[0], range[1]); + const targetHeight = randRange(minHeight * 100, maxHeight * 100); + const riseDuration = randRange(30, 40); + const easing = pickOne([ + 'ease-in-out', + 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', + 'cubic-bezier(0.42, 0, 0.58, 1)' + ]) || 'ease-in-out'; + const riseName = `easter_egg_rise_${Date.now()}`; + sheet.insertRule(`@keyframes ${riseName} { 0% { height: 0%; } 100% { height: ${targetHeight}%; } }`, sheet.cssRules.length); + container.style.animation = `${riseName} ${riseDuration}s ${easing} forwards`; + + const directionSets = [ + [1, 1, -1], + [1, -1, -1], + [-1, 1, 1], + [-1, -1, 1] + ]; + const directions = pickOne(directionSets) || [1, -1, 1]; + const colors = [ + 'rgba(135, 206, 250, 0.35)', + 'rgba(100, 181, 246, 0.45)', + 'rgba(33, 150, 243, 0.4)' + ]; + + waves.forEach((wave, index) => { + const svgData = this.buildFloodWaveShape(index); + const color = colors[index % colors.length]; + const svg = ``; + const encoded = encodeURIComponent(svg); + wave.style.backgroundImage = `url("data:image/svg+xml,${encoded}")`; + wave.style.backgroundSize = `${svgData.waveWidth}px 100%`; + + const animationName = `easter_egg_wave_${index}_${Date.now()}`; + const startPosition = randInt(-200, 200); + const distance = svgData.waveWidth * (directions[index] || 1); + const duration = index === 0 ? randRange(16, 22) : index === 1 ? randRange(11, 16) : randRange(7, 12); + const delay = randRange(0, 1.5); + sheet.insertRule(`@keyframes ${animationName} { 0% { background-position-x: ${startPosition}px; } 100% { background-position-x: ${startPosition + distance}px; } }`, sheet.cssRules.length); + wave.style.animation = `${animationName} ${duration}s linear infinite`; + wave.style.animationDelay = `${delay}s`; + wave.style.backgroundPositionX = `${startPosition}px`; + }); + }, + + buildFloodWaveShape(layerIndex) { + const baseHeight = 180 + layerIndex * 12; + const cycles = 4; + let path = `M0,${baseHeight}`; + let currentX = 0; + let previousAmplitude = randRange(40, 80); + + for (let i = 0; i < cycles; i++) { + const waveLength = randRange(700, 900); + const minAmp = Math.max(20, previousAmplitude - 20); + const maxAmp = Math.min(90, previousAmplitude + 20); + const amplitude = randRange(minAmp, maxAmp); + previousAmplitude = amplitude; + const halfWave = waveLength / 2; + + const peakX = currentX + halfWave / 2; + path += ` Q${peakX},${baseHeight - amplitude} ${currentX + halfWave},${baseHeight}`; + + const troughX = currentX + halfWave + halfWave / 2; + path += ` Q${troughX},${baseHeight + amplitude} ${currentX + waveLength},${baseHeight}`; + + currentX += waveLength; + } + + path += ` L${currentX},1000 L0,1000 Z`; + return { + path, + width: currentX, + waveWidth: currentX / cycles + }; + }, + + clearFloodAnimations() { + const container = this.$refs.easterEggWaterContainer; + if (!container) { + return; + } + container.style.animation = 'none'; + container.style.transition = 'none'; + container.style.height = '0%'; + const waves = container.querySelectorAll('.wave'); + waves.forEach((wave) => { + wave.style.animation = 'none'; + wave.style.backgroundImage = ''; + wave.style.backgroundSize = ''; + wave.style.backgroundPositionX = '0px'; + }); + }, + + teardownEasterEggStyle() { + if (this.easterEgg.styleNode && this.easterEgg.styleNode.parentNode) { + this.easterEgg.styleNode.parentNode.removeChild(this.easterEgg.styleNode); + } + this.easterEgg.styleNode = null; + }, + + destroyEasterEggEffect() { + if (this.easterEgg.cleanupTimer) { + clearTimeout(this.easterEgg.cleanupTimer); + this.easterEgg.cleanupTimer = null; + } + if (this.easterEgg.effect === 'flood' && this.easterEgg.active && !this.easterEgg.retreating) { + this.startFloodRetreat(); + return; + } + if (this.easterEgg.effect === 'flood' && this.easterEgg.retreating) { + return; + } + this.finishEasterEggCleanup(); + }, + + startFloodRetreat() { + const container = this.$refs.easterEggWaterContainer; + if (!container) { + this.finishEasterEggCleanup(); + return; + } + this.easterEgg.retreating = true; + const measuredHeight = container.offsetHeight || container.clientHeight; + const computedHeight = window.getComputedStyle(container).height; + const currentHeight = measuredHeight + ? `${measuredHeight}px` + : (computedHeight && computedHeight !== 'auto' ? computedHeight : '0px'); + container.style.animation = 'none'; + container.style.transition = 'none'; + container.style.height = currentHeight; + void container.offsetHeight; + const retreatDuration = 8; + container.style.transition = `height ${retreatDuration}s ease-in-out`; + requestAnimationFrame(() => { + container.style.height = '0px'; + }); + this.easterEgg.cleanupTimer = setTimeout(() => { + container.style.transition = 'none'; + this.clearFloodAnimations(); + this.teardownEasterEggStyle(); + this.easterEgg.active = false; + this.easterEgg.effect = null; + this.easterEgg.retreating = false; + }, retreatDuration * 1000); + }, + + finishEasterEggCleanup() { + this.clearFloodAnimations(); + this.teardownEasterEggStyle(); + this.easterEgg.active = false; + this.easterEgg.effect = null; + this.easterEgg.retreating = false; + }, + // 格式化token显示(修复NaN问题) formatTokenCount(tokens) { // 确保tokens是数字,防止NaN diff --git a/static/index.html b/static/index.html index abd450c..3333a73 100644 --- a/static/index.html +++ b/static/index.html @@ -823,6 +823,18 @@ +