feat: add hidden easter egg flood effect
This commit is contained in:
parent
6e8321cf7e
commit
afd1bb9d28
@ -41,6 +41,7 @@ from modules.todo_manager import TodoManager
|
|||||||
from modules.sub_agent_manager import SubAgentManager
|
from modules.sub_agent_manager import SubAgentManager
|
||||||
from modules.webpage_extractor import extract_webpage_content, tavily_extract
|
from modules.webpage_extractor import extract_webpage_content, tavily_extract
|
||||||
from modules.ocr_client import OCRClient
|
from modules.ocr_client import OCRClient
|
||||||
|
from modules.easter_egg_manager import EasterEggManager
|
||||||
from core.tool_config import TOOL_CATEGORIES
|
from core.tool_config import TOOL_CATEGORIES
|
||||||
from utils.api_client import DeepSeekClient
|
from utils.api_client import DeepSeekClient
|
||||||
from utils.context_manager import ContextManager
|
from utils.context_manager import ContextManager
|
||||||
@ -84,6 +85,7 @@ class MainTerminal:
|
|||||||
project_path=self.project_path,
|
project_path=self.project_path,
|
||||||
data_dir=str(self.data_dir)
|
data_dir=str(self.data_dir)
|
||||||
)
|
)
|
||||||
|
self.easter_egg_manager = EasterEggManager()
|
||||||
self._announced_sub_agent_tasks = set()
|
self._announced_sub_agent_tasks = set()
|
||||||
|
|
||||||
# 聚焦文件管理
|
# 聚焦文件管理
|
||||||
@ -95,8 +97,12 @@ class MainTerminal:
|
|||||||
self.pending_modify_request = None # {"path": str}
|
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_tools = set()
|
||||||
|
self.disabled_notice_tools = set()
|
||||||
self._refresh_disabled_tools()
|
self._refresh_disabled_tools()
|
||||||
|
|
||||||
# 新增:自动开始新对话
|
# 新增:自动开始新对话
|
||||||
@ -402,7 +408,7 @@ class MainTerminal:
|
|||||||
snapshot.append({
|
snapshot.append({
|
||||||
"id": key,
|
"id": key,
|
||||||
"label": category.label,
|
"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),
|
"tools": list(category.tools),
|
||||||
})
|
})
|
||||||
return snapshot
|
return snapshot
|
||||||
@ -410,18 +416,23 @@ class MainTerminal:
|
|||||||
def _refresh_disabled_tools(self) -> None:
|
def _refresh_disabled_tools(self) -> None:
|
||||||
"""刷新禁用工具列表 / Refresh disabled tool set."""
|
"""刷新禁用工具列表 / Refresh disabled tool set."""
|
||||||
disabled = set()
|
disabled = set()
|
||||||
|
notice = set()
|
||||||
for key, enabled in self.tool_category_states.items():
|
for key, enabled in self.tool_category_states.items():
|
||||||
if not enabled:
|
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_tools = disabled
|
||||||
|
self.disabled_notice_tools = notice
|
||||||
|
|
||||||
def _format_disabled_tool_notice(self) -> Optional[str]:
|
def _format_disabled_tool_notice(self) -> Optional[str]:
|
||||||
"""生成禁用工具提示信息 / Format disabled tool notice."""
|
"""生成禁用工具提示信息 / Format disabled tool notice."""
|
||||||
if not self.disabled_tools:
|
if not self.disabled_notice_tools:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
lines = ["=== 工具可用性提醒 ==="]
|
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(f"{tool_name}:已被用户禁用")
|
||||||
lines.append("=== 提示结束 ===")
|
lines.append("=== 提示结束 ===")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
@ -1456,6 +1467,23 @@ class MainTerminal:
|
|||||||
"required": []
|
"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:
|
if self.disabled_tools:
|
||||||
@ -2050,6 +2078,9 @@ class MainTerminal:
|
|||||||
agent_id=arguments.get("agent_id")
|
agent_id=arguments.get("agent_id")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif tool_name == "trigger_easter_egg":
|
||||||
|
result = self.easter_egg_manager.trigger_effect(arguments.get("effect"))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
result = {"success": False, "error": f"未知工具: {tool_name}"}
|
result = {"success": False, "error": f"未知工具: {tool_name}"}
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,17 @@ from typing import Dict, List
|
|||||||
class ToolCategory:
|
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.label = label
|
||||||
self.tools = tools
|
self.tools = tools
|
||||||
|
self.default_enabled = default_enabled
|
||||||
|
self.silent_when_disabled = silent_when_disabled
|
||||||
|
|
||||||
|
|
||||||
TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
||||||
@ -60,4 +68,10 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
|||||||
label="子智能体",
|
label="子智能体",
|
||||||
tools=["create_sub_agent", "wait_sub_agent", "close_sub_agent"],
|
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,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
68
modules/easter_egg_manager.py
Normal file
68
modules/easter_egg_manager.py
Normal file
@ -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())
|
||||||
263
static/app.js
263
static/app.js
@ -103,7 +103,8 @@ const TOOL_ICON_MAP = Object.freeze({
|
|||||||
unfocus_file: 'eye',
|
unfocus_file: 'eye',
|
||||||
update_memory: 'brain',
|
update_memory: 'brain',
|
||||||
wait_sub_agent: 'clock',
|
wait_sub_agent: 'clock',
|
||||||
web_search: 'search'
|
web_search: 'search',
|
||||||
|
trigger_easter_egg: 'sparkles'
|
||||||
});
|
});
|
||||||
|
|
||||||
const TOOL_CATEGORY_ICON_MAP = Object.freeze({
|
const TOOL_CATEGORY_ICON_MAP = Object.freeze({
|
||||||
@ -114,9 +115,25 @@ const TOOL_CATEGORY_ICON_MAP = Object.freeze({
|
|||||||
terminal_command: 'terminal',
|
terminal_command: 'terminal',
|
||||||
memory: 'brain',
|
memory: 'brain',
|
||||||
todo: 'stickyNote',
|
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) {
|
function injectScriptSequentially(urls, onSuccess, onFailure) {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const tryLoad = () => {
|
const tryLoad = () => {
|
||||||
@ -303,6 +320,13 @@ async function bootstrapApp() {
|
|||||||
todoList: null,
|
todoList: null,
|
||||||
icons: ICONS,
|
icons: ICONS,
|
||||||
toolCategoryIcons: TOOL_CATEGORY_ICON_MAP,
|
toolCategoryIcons: TOOL_CATEGORY_ICON_MAP,
|
||||||
|
easterEgg: {
|
||||||
|
active: false,
|
||||||
|
effect: null,
|
||||||
|
cleanupTimer: null,
|
||||||
|
styleNode: null,
|
||||||
|
retreating: false
|
||||||
|
},
|
||||||
|
|
||||||
// 右键菜单相关
|
// 右键菜单相关
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
@ -390,6 +414,8 @@ async function bootstrapApp() {
|
|||||||
clearInterval(this.subAgentPollTimer);
|
clearInterval(this.subAgentPollTimer);
|
||||||
this.subAgentPollTimer = null;
|
this.subAgentPollTimer = null;
|
||||||
}
|
}
|
||||||
|
this.destroyEasterEggEffect();
|
||||||
|
this.finishEasterEggCleanup();
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -1075,6 +1101,9 @@ async function bootstrapApp() {
|
|||||||
if (data.result !== undefined) {
|
if (data.result !== undefined) {
|
||||||
targetAction.tool.result = data.result;
|
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) {
|
if (data.message !== undefined) {
|
||||||
targetAction.tool.message = data.message;
|
targetAction.tool.message = data.message;
|
||||||
}
|
}
|
||||||
@ -2516,7 +2545,7 @@ async function bootstrapApp() {
|
|||||||
if (!this.quickMenuOpen) {
|
if (!this.quickMenuOpen) {
|
||||||
this.quickMenuOpen = true;
|
this.quickMenuOpen = true;
|
||||||
}
|
}
|
||||||
this.loadToolSettings();
|
this.loadToolSettings(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -3233,6 +3262,234 @@ async function bootstrapApp() {
|
|||||||
document.body.style.cursor = '';
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgData.width} 1000" preserveAspectRatio="none"><path d="${svgData.path}" fill="${color}"/></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问题)
|
// 格式化token显示(修复NaN问题)
|
||||||
formatTokenCount(tokens) {
|
formatTokenCount(tokens) {
|
||||||
// 确保tokens是数字,防止NaN
|
// 确保tokens是数字,防止NaN
|
||||||
|
|||||||
@ -823,6 +823,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="easter-egg-overlay"
|
||||||
|
v-show="easterEgg.active"
|
||||||
|
:class="{ active: easterEgg.active }"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="easter-egg-water" ref="easterEggWater">
|
||||||
|
<div class="easter-egg-water-container" ref="easterEggWaterContainer">
|
||||||
|
<div class="wave wave1"></div>
|
||||||
|
<div class="wave wave2"></div>
|
||||||
|
<div class="wave wave3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="context-menu"
|
<div class="context-menu"
|
||||||
v-if="contextMenu.visible"
|
v-if="contextMenu.visible"
|
||||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||||
|
|||||||
@ -2612,3 +2612,59 @@ o-files {
|
|||||||
.personal-page-fade-leave-to {
|
.personal-page-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 彩蛋灌水特效 */
|
||||||
|
.easter-egg-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
z-index: 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-egg-overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-egg-water {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-egg-water-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0%;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-egg-water-container .wave {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 200%;
|
||||||
|
height: 100%;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-position: bottom;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-egg-water-container .wave.wave1 {
|
||||||
|
filter: blur(0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-egg-water-container .wave.wave2 {
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-egg-water-container .wave.wave3 {
|
||||||
|
filter: blur(1.5px);
|
||||||
|
}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ from modules.todo_manager import TodoManager
|
|||||||
from modules.sub_agent_manager import SubAgentManager
|
from modules.sub_agent_manager import SubAgentManager
|
||||||
from modules.webpage_extractor import extract_webpage_content, tavily_extract
|
from modules.webpage_extractor import extract_webpage_content, tavily_extract
|
||||||
from modules.ocr_client import OCRClient
|
from modules.ocr_client import OCRClient
|
||||||
|
from modules.easter_egg_manager import EasterEggManager
|
||||||
from core.tool_config import TOOL_CATEGORIES
|
from core.tool_config import TOOL_CATEGORIES
|
||||||
from utils.api_client import DeepSeekClient
|
from utils.api_client import DeepSeekClient
|
||||||
from utils.context_manager import ContextManager
|
from utils.context_manager import ContextManager
|
||||||
@ -85,6 +86,7 @@ class MainTerminal:
|
|||||||
project_path=self.project_path,
|
project_path=self.project_path,
|
||||||
data_dir=str(self.data_dir)
|
data_dir=str(self.data_dir)
|
||||||
)
|
)
|
||||||
|
self.easter_egg_manager = EasterEggManager()
|
||||||
self._announced_sub_agent_tasks = set()
|
self._announced_sub_agent_tasks = set()
|
||||||
|
|
||||||
# 聚焦文件管理
|
# 聚焦文件管理
|
||||||
@ -96,8 +98,12 @@ class MainTerminal:
|
|||||||
self.pending_modify_request = None # {"path": str}
|
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_tools = set()
|
||||||
|
self.disabled_notice_tools = set()
|
||||||
self._refresh_disabled_tools()
|
self._refresh_disabled_tools()
|
||||||
|
|
||||||
# 新增:自动开始新对话
|
# 新增:自动开始新对话
|
||||||
@ -403,7 +409,7 @@ class MainTerminal:
|
|||||||
snapshot.append({
|
snapshot.append({
|
||||||
"id": key,
|
"id": key,
|
||||||
"label": category.label,
|
"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),
|
"tools": list(category.tools),
|
||||||
})
|
})
|
||||||
return snapshot
|
return snapshot
|
||||||
@ -411,18 +417,23 @@ class MainTerminal:
|
|||||||
def _refresh_disabled_tools(self) -> None:
|
def _refresh_disabled_tools(self) -> None:
|
||||||
"""刷新禁用工具列表 / Refresh disabled tool set."""
|
"""刷新禁用工具列表 / Refresh disabled tool set."""
|
||||||
disabled = set()
|
disabled = set()
|
||||||
|
notice = set()
|
||||||
for key, enabled in self.tool_category_states.items():
|
for key, enabled in self.tool_category_states.items():
|
||||||
if not enabled:
|
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_tools = disabled
|
||||||
|
self.disabled_notice_tools = notice
|
||||||
|
|
||||||
def _format_disabled_tool_notice(self) -> Optional[str]:
|
def _format_disabled_tool_notice(self) -> Optional[str]:
|
||||||
"""生成禁用工具提示信息 / Format disabled tool notice."""
|
"""生成禁用工具提示信息 / Format disabled tool notice."""
|
||||||
if not self.disabled_tools:
|
if not self.disabled_notice_tools:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
lines = ["=== 工具可用性提醒 ==="]
|
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(f"{tool_name}:已被用户禁用")
|
||||||
lines.append("=== 提示结束 ===")
|
lines.append("=== 提示结束 ===")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
@ -1428,6 +1439,23 @@ class MainTerminal:
|
|||||||
"required": []
|
"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:
|
if self.disabled_tools:
|
||||||
@ -2007,6 +2035,9 @@ class MainTerminal:
|
|||||||
timeout_seconds=arguments.get("timeout_seconds")
|
timeout_seconds=arguments.get("timeout_seconds")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif tool_name == "trigger_easter_egg":
|
||||||
|
result = self.easter_egg_manager.trigger_effect(arguments.get("effect"))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
result = {"success": False, "error": f"未知工具: {tool_name}"}
|
result = {"success": False, "error": f"未知工具: {tool_name}"}
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,17 @@ from typing import Dict, List
|
|||||||
class ToolCategory:
|
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.label = label
|
||||||
self.tools = tools
|
self.tools = tools
|
||||||
|
self.default_enabled = default_enabled
|
||||||
|
self.silent_when_disabled = silent_when_disabled
|
||||||
|
|
||||||
|
|
||||||
TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
||||||
@ -56,4 +64,14 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
|||||||
label="待办事项",
|
label="待办事项",
|
||||||
tools=["todo_create", "todo_update_task", "todo_finish", "todo_finish_confirm"],
|
tools=["todo_create", "todo_update_task", "todo_finish", "todo_finish_confirm"],
|
||||||
),
|
),
|
||||||
|
"sub_agent": 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,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user