Merge branch 'main' into chore/readme

This commit is contained in:
Puzhen Zhang 2026-01-20 00:37:44 +00:00 committed by GitHub
commit 8a76298cc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 219 additions and 159 deletions

View file

@ -20,6 +20,8 @@ jobs:
arch: x64
- os: windows-latest
arch: x64
- os: ubuntu-latest
arch: x64
steps:
- name: Checkout Code
@ -46,6 +48,13 @@ jobs:
- name: Install Dependencies
run: npm install
# Install libfuse2 for Linux AppImage builds
- name: Install libfuse2 (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libfuse2
# Step for macOS builds with signing
- name: Build Release Files (macOS with signing)
if: runner.os == 'macOS'
@ -78,6 +87,19 @@ jobs:
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
USE_NPM_INSTALL_BUN: 'true'
# Step for Linux builds
- name: Build Release Files (Linux)
if: runner.os == 'Linux'
timeout-minutes: 90
run: npm run build:linux
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
VITE_STACK_PROJECT_ID: ${{ secrets.VITE_STACK_PROJECT_ID }}
VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
USE_NPM_INSTALL_BUN: 'true'
- name: Upload Artifact (macOS - dmg only)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v6
@ -95,13 +117,22 @@ jobs:
path: |
release/*.exe
retention-days: 5
- name: Upload Artifact (Linux - AppImage only)
if: runner.os == 'Linux'
uses: actions/upload-artifact@v6
with:
name: release-${{ matrix.os }}-${{ matrix.arch }}
path: |
release/*.AppImage
retention-days: 5
merge-release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Create directories
run: |
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 release/linux-x64
# Download all artifacts with correct names
- name: Download mac-x64 artifact
@ -122,7 +153,13 @@ jobs:
name: release-windows-latest-x64
path: temp-win-x64
# Move only dmg files for macOS and exe files for Windows
- name: Download linux-x64 artifact
uses: actions/download-artifact@v7
with:
name: release-ubuntu-latest-x64
path: temp-linux-x64
# Move only dmg files for macOS, exe files for Windows, and AppImage for Linux
- name: Move files to clean folders
shell: bash
run: |
@ -146,3 +183,10 @@ jobs:
else
find temp-win-x64 -name "*.exe" -exec mv {} release/win-x64/ \; || true
fi
# linux-x64 - only move AppImage files
if [ -d "temp-linux-x64/release" ]; then
find temp-linux-x64/release -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
else
find temp-linux-x64 -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
fi

View file

@ -115,15 +115,12 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
@router.post("/chat", name="start chat")
@traceroot.trace()
async def post(data: Chat, request: Request):
request_start_time = time.time()
chat_logger.info(
"Starting new chat session",
extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
)
task_lock = get_or_create_task_lock(data.project_id)
# Store request start time in task_lock for downstream timing
task_lock.request_start_time = request_start_time
# Set user-specific environment path for this thread
set_user_env_path(data.env_path)

View file

@ -238,18 +238,9 @@ def build_context_for_workforce(task_lock: TaskLock, options: Chat) -> str:
@sync_step
@traceroot.trace()
async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
import time as time_module
# === TIMING: step_solve started ===
step_solve_start = time_module.time()
request_start_time = getattr(task_lock, 'request_start_time', step_solve_start)
time_since_request = (step_solve_start - request_start_time) * 1000
logger.info(f"⏱️ [TIMING] step_solve started, {time_since_request:.2f}ms since request received")
start_event_loop = True
# === TIMING: Task lock initialization ===
init_start = time_module.time()
# Initialize task_lock attributes
if not hasattr(task_lock, 'conversation_history'):
task_lock.conversation_history = []
if not hasattr(task_lock, 'last_task_result'):
@ -258,24 +249,19 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
task_lock.question_agent = None
if not hasattr(task_lock, 'summary_generated'):
task_lock.summary_generated = False
init_time = (time_module.time() - init_start) * 1000
logger.info(f"⏱️ [TIMING] Task lock attrs initialized in {init_time:.2f}ms")
# Create or reuse persistent question_agent
# === TIMING: question_agent creation ===
question_agent_start = time_module.time()
if task_lock.question_agent is None:
task_lock.question_agent = question_confirm_agent(options)
question_agent_time = (time_module.time() - question_agent_start) * 1000
logger.info(f"⏱️ [TIMING] question_confirm_agent created in {question_agent_time:.2f}ms")
else:
logger.info(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
logger.debug(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
question_agent = task_lock.question_agent
# Other variables
camel_task = None
workforce = None
mcp = None
last_completed_task_result = "" # Track the last completed task result
summary_task_content = "" # Track task summary
loop_iteration = 0
@ -312,11 +298,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
logger.info(f"[LIFECYCLE] Breaking out of step_solve loop due to client disconnect")
break
try:
# === TIMING: Waiting for queue item ===
queue_wait_start = time_module.time()
item = await task_lock.get_queue()
queue_wait_time = (time_module.time() - queue_wait_start) * 1000
logger.info(f"⏱️ [TIMING] Got item from queue in {queue_wait_time:.2f}ms, action={item.action}")
except Exception as e:
logger.error("Error getting item from queue", extra={"project_id": options.project_id, "task_id": options.task_id, "error": str(e)}, exc_info=True)
# Continue waiting instead of breaking on queue error
@ -352,25 +334,13 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
})
continue
# === TIMING: Complexity determination ===
complexity_start = time_module.time()
time_since_request_to_complexity = (complexity_start - request_start_time) * 1000
logger.info(f"⏱️ [TIMING] Starting complexity check, {time_since_request_to_complexity:.2f}ms since request")
# Simplified logic: attachments mean workforce, otherwise let agent decide
# Determine task complexity: attachments mean workforce, otherwise let agent decide
is_complex_task: bool
if len(options.attaches) > 0:
# Questions with attachments always need workforce
is_complex_task = True
logger.info(f"[NEW-QUESTION] Has attachments, treating as complex task")
complexity_time = (time_module.time() - complexity_start) * 1000
logger.info(f"⏱️ [TIMING] Complexity check (has attachments) completed in {complexity_time:.2f}ms")
else:
logger.info(f"[NEW-QUESTION] Calling question_confirm to determine complexity")
question_confirm_start = time_module.time()
is_complex_task = await question_confirm(question_agent, question, task_lock)
question_confirm_time = (time_module.time() - question_confirm_start) * 1000
logger.info(f"⏱️ [TIMING] question_confirm completed in {question_confirm_time:.2f}ms, is_complex={is_complex_task}")
logger.info(f"[NEW-QUESTION] question_confirm result: is_complex={is_complex_task}")
if not is_complex_task:
@ -409,83 +379,36 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
except Exception as e:
logger.error(f"Error cleaning up folder: {e}")
else:
# === TIMING: Complex task processing ===
complex_task_start = time_module.time()
time_since_request_to_complex = (complex_task_start - request_start_time) * 1000
logger.info(f"⏱️ [TIMING] Complex task processing started, {time_since_request_to_complex:.2f}ms since request")
logger.info(f"[NEW-QUESTION] 🔧 Complex task, creating workforce and decomposing")
logger.info(f"[NEW-QUESTION] Complex task, creating workforce and decomposing")
# Update the sync_step with new task_id
if hasattr(item, 'new_task_id') and item.new_task_id:
set_current_task_id(options.project_id, item.new_task_id)
# Reset summary generation flag for new tasks to ensure proper summaries
task_lock.summary_generated = False
logger.info("[NEW-QUESTION] Reset summary_generated flag for new task", extra={"project_id": options.project_id, "new_task_id": item.new_task_id})
logger.info(f"[NEW-QUESTION] Sending 'confirmed' SSE to frontend")
yield sse_json("confirmed", {"question": question})
# === TIMING: Context building ===
context_build_start = time_module.time()
logger.info(f"[NEW-QUESTION] Building context for coordinator")
context_for_coordinator = build_context_for_workforce(task_lock, options)
context_build_time = (time_module.time() - context_build_start) * 1000
logger.info(f"⏱️ [TIMING] Context building completed in {context_build_time:.2f}ms")
# Check if workforce exists - if so, reuse it (agents are preserved)
# Otherwise create new workforce
# Check if workforce exists - if so, reuse it; otherwise create new workforce
if workforce is not None:
logger.info(f"[NEW-QUESTION] 🔄 Workforce exists (id={id(workforce)}), state={workforce._state.name}, _running={workforce._running}")
logger.info(f"[NEW-QUESTION] ✅ Reusing existing workforce with preserved agents")
# Workforce is already stopped from skip_task, ready for new decomposition
logger.debug(f"[NEW-QUESTION] Reusing existing workforce (id={id(workforce)})")
else:
# === TIMING: Workforce construction ===
workforce_construct_start = time_module.time()
logger.info(f"[NEW-QUESTION] 🏭 Creating NEW workforce instance (workforce=None)")
logger.info(f"[NEW-QUESTION] Creating NEW workforce instance")
(workforce, mcp) = await construct_workforce(options)
workforce_construct_time = (time_module.time() - workforce_construct_start) * 1000
logger.info(f"⏱️ [TIMING] Workforce construction completed in {workforce_construct_time:.2f}ms")
logger.info(f"[NEW-QUESTION] ✅ NEW Workforce instance created, id={id(workforce)}")
for new_agent in options.new_agents:
workforce.add_single_agent_worker(
format_agent_description(new_agent), await new_agent_model(new_agent, options)
)
task_lock.status = Status.confirmed
# === TIMING: Task creation ===
task_create_start = time_module.time()
# Create camel_task for the question
clean_task_content = question + options.summary_prompt
camel_task = Task(content=clean_task_content, id=options.task_id)
if len(options.attaches) > 0:
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
# If camel_task already exists (from previous paused task), add new question as subtask
# Otherwise, create a new camel_task
if camel_task is not None:
logger.info(f"[NEW-QUESTION] 🔄 camel_task exists (id={camel_task.id}), adding new question as context")
# Update the task content with new question
clean_task_content = question + options.summary_prompt
logger.info(f"[NEW-QUESTION] Updating existing camel_task content with new question")
# We keep the existing task structure but update content for new decomposition
camel_task = Task(content=clean_task_content, id=options.task_id)
if len(options.attaches) > 0:
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
else:
clean_task_content = question + options.summary_prompt
logger.info(f"[NEW-QUESTION] Creating NEW camel_task with id={options.task_id}")
camel_task = Task(content=clean_task_content, id=options.task_id)
if len(options.attaches) > 0:
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
task_create_time = (time_module.time() - task_create_start) * 1000
logger.info(f"⏱️ [TIMING] Task object created in {task_create_time:.2f}ms")
# === TIMING: Task decomposition start ===
decomposition_start = time_module.time()
time_since_request_to_decompose = (decomposition_start - request_start_time) * 1000
logger.info(f"⏱️ [TIMING] Starting task decomposition, {time_since_request_to_decompose:.2f}ms since request")
# Store decomposition start time in task_lock for downstream tracking
task_lock.decomposition_start_time = decomposition_start
# Stream decomposition in background so queue items (decompose_text) are processed immediately
logger.info(f"[NEW-QUESTION] 🧩 Starting task decomposition via workforce.eigent_make_sub_tasks")
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": "", "first_token_logged": False}
# Stream decomposition in background
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
state_holder: dict[str, Any] = {"sub_tasks": [], "summary_task": ""}
def on_stream_batch(new_tasks: list[Task], is_final: bool = False):
@ -496,8 +419,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
def on_stream_text(chunk):
try:
# With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
# We need to calculate the delta to send only new content to frontend
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
last_content = stream_state["last_content"]
@ -510,12 +431,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
stream_state["last_content"] = accumulated_content
if delta_content:
# === TIMING: Log TTFT (Time to First Token) ===
if not stream_state["first_token_logged"]:
stream_state["first_token_logged"] = True
ttft = (time_module.time() - task_lock.decomposition_start_time) * 1000
logger.info(f"⏱️ [TIMING] 🚀 TTFT (Time to First Token): {ttft:.2f}ms - First streaming token received for task decomposition")
asyncio.run_coroutine_threadsafe(
task_lock.put_queue(
ActionDecomposeTextData(
@ -534,10 +449,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
async def run_decomposition():
nonlocal camel_task, summary_task_content
try:
# === TIMING: LLM decomposition call ===
llm_decompose_start = time_module.time()
decomposition_start_time = getattr(task_lock, 'decomposition_start_time', llm_decompose_start)
sub_tasks = await asyncio.to_thread(
workforce.eigent_make_sub_tasks,
camel_task,
@ -546,60 +457,44 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
on_stream_text,
)
llm_decompose_time = (time_module.time() - llm_decompose_start) * 1000
total_decompose_time = (time_module.time() - decomposition_start_time) * 1000
logger.info(f"⏱️ [TIMING] LLM decomposition completed in {llm_decompose_time:.2f}ms (total decompose phase: {total_decompose_time:.2f}ms)")
if stream_state["subtasks"]:
sub_tasks = stream_state["subtasks"]
state_holder["sub_tasks"] = sub_tasks
logger.info(f"[NEW-QUESTION] ✅ Task decomposed into {len(sub_tasks)} subtasks")
logger.info(f"Task decomposed into {len(sub_tasks)} subtasks")
try:
setattr(task_lock, "decompose_sub_tasks", sub_tasks)
except Exception:
pass
# === TIMING: Summary generation ===
summary_start = time_module.time()
logger.info(f"[NEW-QUESTION] Generating task summary")
# Generate task summary
summary_task_agent = task_summary_agent(options)
try:
summary_task_content = await asyncio.wait_for(
summary_task(summary_task_agent, camel_task), timeout=10
)
summary_time = (time_module.time() - summary_start) * 1000
task_lock.summary_generated = True
logger.info(f"⏱️ [TIMING] Summary generation completed in {summary_time:.2f}ms")
logger.info("[NEW-QUESTION] ✅ Summary generated successfully", extra={"project_id": options.project_id})
except asyncio.TimeoutError:
logger.warning("summary_task timeout", extra={"project_id": options.project_id, "task_id": options.task_id})
task_lock.summary_generated = True
fallback_name = "Task"
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
if content_preview is None:
content_preview = ""
summary_task_content = (
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
)
summary_task_content = f"{fallback_name}|{summary_task_content}"
summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
summary_task_content = f"Task|{summary_task_content}"
except Exception:
task_lock.summary_generated = True
fallback_name = "Task"
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
if content_preview is None:
content_preview = ""
summary_task_content = (
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
)
summary_task_content = f"{fallback_name}|{summary_task_content}"
summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
summary_task_content = f"Task|{summary_task_content}"
state_holder["summary_task"] = summary_task_content
try:
setattr(task_lock, "summary_task_content", summary_task_content)
except Exception:
pass
logger.info(f"[NEW-QUESTION] 📤 Sending to_sub_tasks SSE to frontend (task card)")
logger.info(f"[NEW-QUESTION] to_sub_tasks data: task_id={camel_task.id}, summary={summary_task_content[:50]}..., subtasks_count={len(camel_task.subtasks)}")
payload = {
"project_id": options.project_id,
"task_id": options.task_id,
@ -609,12 +504,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
"summary_task": summary_task_content,
}
await task_lock.put_queue(ActionDecomposeProgressData(data=payload))
logger.info(f"[NEW-QUESTION] ✅ to_sub_tasks SSE sent")
# === TIMING: Total time from request to decomposition complete ===
request_start = getattr(task_lock, 'request_start_time', decomposition_start_time)
total_request_to_decompose = (time_module.time() - request_start) * 1000
logger.info(f"⏱️ [TIMING] ===== TOTAL: Request → Decomposition Complete: {total_request_to_decompose:.2f}ms =====")
except Exception as e:
logger.error(f"Error in background decomposition: {e}", exc_info=True)
@ -854,9 +743,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
logger.info(f"[LIFECYCLE] Multi-turn: building context for workforce")
context_for_multi_turn = build_context_for_workforce(task_lock, options)
logger.info(f"[LIFECYCLE] Multi-turn: calling workforce.handle_decompose_append_task for new task decomposition")
multi_turn_decompose_start = time_module.time()
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": "", "first_token_logged": False, "start_time": multi_turn_decompose_start}
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
def on_stream_batch(new_tasks: list[Task], is_final: bool = False):
fresh_tasks = [t for t in new_tasks if t.id not in stream_state["seen_ids"]]
@ -866,8 +753,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
def on_stream_text(chunk):
try:
# With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
# We need to calculate the delta to send only new content to frontend
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
last_content = stream_state["last_content"]
@ -879,12 +764,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
stream_state["last_content"] = accumulated_content
if delta_content:
# === TIMING: Log TTFT (Time to First Token) for multi-turn ===
if not stream_state["first_token_logged"]:
stream_state["first_token_logged"] = True
ttft = (time_module.time() - stream_state["start_time"]) * 1000
logger.info(f"⏱️ [TIMING] 🚀 TTFT (Time to First Token): {ttft:.2f}ms - First streaming token received for multi-turn task decomposition")
asyncio.run_coroutine_threadsafe(
task_lock.put_queue(
ActionDecomposeTextData(
@ -987,6 +866,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
elif item.action == Action.search_mcp:
yield sse_json("search_mcp", item.data)
elif item.action == Action.install_mcp:
if mcp is None:
logger.error(f"Cannot install MCP: mcp agent not initialized for project {options.project_id}")
yield sse_json("error", {"message": "MCP agent not initialized. Please start a complex task first."})
continue
task = asyncio.create_task(install_mcp(mcp, item))
task_lock.add_background_task(task)
elif item.action == Action.terminal:

View file

@ -64,6 +64,13 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
safe_mode=safe_mode,
allowed_commands=allowed_commands,
clone_current_env=clone_current_env,
install_dependencies=[
"pandas",
"numpy",
"matplotlib",
"requests",
"openpyxl",
],
)
# Auto-register with TaskLock for cleanup when task ends

View file

@ -355,6 +355,7 @@ class Workforce(BaseWorkforce):
f"Task {task.id} will not be properly tracked on frontend. "
f"Available workers: {[c.node_id for c in self._children if hasattr(c, 'node_id')]}"
)
else:
await task_lock.put_queue(
ActionAssignTaskData(
action=Action.assign_task,

View file

@ -67,7 +67,6 @@
}
},
"win": {
"certificateFile": null,
"icon": "build/icon.ico",
"artifactName": "${productName}.Setup.${version}.exe",
"target": [

View file

@ -112,6 +112,32 @@ app.commandLine.appendSwitch('max_old_space_size', '4096');
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
app.commandLine.appendSwitch('renderer-process-limit', '8');
// ==================== Anti-fingerprint settings ====================
// Disable automation controlled indicator to avoid detection
app.commandLine.appendSwitch(
'disable-blink-features',
'AutomationControlled'
);
// Override User Agent to remove Electron/eigent identifiers
// Dynamically generate User Agent based on actual platform and Chrome version
const getPlatformUA = () => {
// Use actual Chrome version from Electron instead of hardcoded value
const chromeVersion = process.versions.chrome || '131.0.0.0';
switch (process.platform) {
case 'darwin':
return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
case 'win32':
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
case 'linux':
return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
default:
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
}
};
const normalUserAgent = getPlatformUA();
app.userAgentFallback = normalUserAgent;
// ==================== protocol privileges ====================
// Register custom protocol privileges before app ready
protocol.registerSchemesAsPrivileged([
@ -1827,6 +1853,15 @@ app.whenReady().then(async () => {
}
}
// ==================== Anti-fingerprint: Set User Agent for all sessions ====================
// Use the same dynamic User Agent as app.userAgentFallback
session.defaultSession.setUserAgent(normalUserAgent);
// Also set for the user_login partition used by webviews
session.fromPartition('persist:user_login').setUserAgent(normalUserAgent);
// And for main_window partition
session.fromPartition('persist:main_window').setUserAgent(normalUserAgent);
log.info('[ANTI-FINGERPRINT] User Agent set for all sessions');
// ==================== download handle ====================
session.defaultSession.on('will-download', (event, item, webContents) => {
item.once('done', (event, state) => {

View file

@ -72,13 +72,108 @@ export class WebViewManager {
backgroundThrottling: true,
offscreen: false,
sandbox: true,
disableBlinkFeatures: 'Accelerated2dCanvas',
disableBlinkFeatures: 'Accelerated2dCanvas,AutomationControlled',
enableBlinkFeatures: 'IdleDetection',
autoplayPolicy: 'document-user-activation-required',
},
})
view.webContents.on('did-finish-load', () => {
// Inject stealth script to avoid bot detection
view.webContents.executeJavaScript(`
// Save original values before overriding to maintain consistency
const originalLanguages = navigator.languages ? [...navigator.languages] : ['en-US', 'en'];
const originalHardwareConcurrency = navigator.hardwareConcurrency || 8;
const originalDeviceMemory = navigator.deviceMemory || 8;
// Hide webdriver property
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
configurable: true
});
// Override plugins with proper PluginArray-like behavior
Object.defineProperty(navigator, 'plugins', {
get: () => {
const plugins = {
length: 3,
0: { name: 'Chrome PDF Plugin', description: 'Portable Document Format', filename: 'internal-pdf-viewer' },
1: { name: 'Chrome PDF Viewer', description: '', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
2: { name: 'Native Client', description: '', filename: 'internal-nacl-plugin' },
item: function(index) { return this[index] || null; },
namedItem: function(name) {
for (let i = 0; i < this.length; i++) {
if (this[i].name === name) return this[i];
}
return null;
},
refresh: function() {},
[Symbol.iterator]: function* () {
for (let i = 0; i < this.length; i++) {
yield this[i];
}
}
};
return plugins;
},
configurable: true
});
// Use original system languages for consistency with other browser data
Object.defineProperty(navigator, 'languages', {
get: () => originalLanguages,
configurable: true
});
// Use original hardwareConcurrency, clamped to common range (4-16) to avoid extreme fingerprints
Object.defineProperty(navigator, 'hardwareConcurrency', {
get: () => Math.min(Math.max(originalHardwareConcurrency, 4), 16),
configurable: true
});
// Use original deviceMemory, clamped to common range (4-16) to avoid extreme fingerprints
Object.defineProperty(navigator, 'deviceMemory', {
get: () => Math.min(Math.max(originalDeviceMemory, 4), 16),
configurable: true
});
// Fix WebGL vendor/renderer for both WebGL and WebGL2
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.';
if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
return getParameter.call(this, parameter);
};
// Also patch WebGL2RenderingContext
if (typeof WebGL2RenderingContext !== 'undefined') {
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.';
if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
return getParameter2.call(this, parameter);
};
}
// Override chrome runtime - real Chrome has window.chrome but runtime is undefined
if (!window.chrome) {
window.chrome = {};
}
// In real Chrome, runtime exists but is undefined outside extensions
// Don't set it to an object, that's detectable
// Hide automation variables
const automationVars = ['__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn',
'__driver_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', 'domAutomation', 'domAutomationController'];
automationVars.forEach(v => {
Object.defineProperty(window, v, {
get: () => undefined,
set: () => {},
configurable: true,
enumerable: false
});
});
// Mouse event handler
window.addEventListener('mousedown', (e) => {
if (!(e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement)) {
e.preventDefault();

View file

@ -630,7 +630,7 @@ const ToolSelect = forwardRef<
key={item.id + item.key + (item.isLocal + "")}
className="h-5 bg-button-tertiery-fill-default flex items-center gap-1 w-auto flex-shrink-0 px-xs"
>
{item.name || item.mcp_name}
{item.name || item.mcp_name || item.key || `tool_${item.id}`}
<div className="flex items-center justify-center bg-button-secondary-fill-disabled rounded-sm">
<X
className="w-4 h-4 cursor-pointer text-button-secondary-icon-disabled"

View file

@ -282,7 +282,7 @@ export function AddWorker({
name: workerName,
type: workerName as AgentNameType,
log: [],
tools: [...selectedTools.map((tool) => tool.name)],
tools: [...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`)],
activeWebviewIds: [],
workerInfo: {
name: workerName,
@ -310,7 +310,7 @@ export function AddWorker({
type: workerName as AgentNameType,
log: [],
tools: [
...selectedTools.map((tool) => tool?.key || tool?.mcp_name || ""),
...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`),
],
activeWebviewIds: [],
workerInfo: {

View file

@ -241,15 +241,14 @@ export default function Workflow({
// Merge all agents
const allAgents = [...taskAssigning, ...base];
// Sort: agents with tasks come first, then agents without tasks
taskAssigning = allAgents.sort((a, b) => {
const sortedAgents = allAgents.sort((a, b) => {
const aHasTasks = a.tasks && a.tasks.length > 0;
const bHasTasks = b.tasks && b.tasks.length > 0;
if (aHasTasks && !bHasTasks) return -1;
if (!aHasTasks && bHasTasks) return 1;
return 0; // Keep original order for agents with same task status
return 0;
});
// taskAssigning = taskAssigning.filter((agent) => agent.tasks.length > 0);
targetData = taskAssigning.map((agent, index) => {
targetData = sortedAgents.map((agent, index) => {
const node = targetData.find((node) => node.id === agent.agent_id);
if (node) {
return {

View file

@ -420,7 +420,7 @@ export function Node({ id, data }: NodeProps) {
</div>
<div
ref={toolsRef}
className="flex-shrink-0 text-text-label text-xs leading-tight min-h-4 font-normal mb-sm pr-3 text-"
className="flex-shrink-0 text-text-label text-xs leading-tight min-h-4 font-normal mb-sm pr-3"
>
{/* {JSON.stringify(data.agent)} */}
{agentToolkits[