<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Acemcp Management</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
}
</script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
[x-cloak] { display: none !important; }
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 transition-colors">
<div x-data="mcpManager()" x-init="init()" class="container mx-auto p-6">
<header class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100" x-text="t('title')"></h1>
<p class="text-gray-600 dark:text-gray-300 mt-2" x-text="t('subtitle')"></p>
</div>
<div class="flex gap-2">
<button @click="toggleTheme()" class="px-3 py-1 rounded text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600">
<span x-show="theme === 'light'">🌙</span>
<span x-show="theme === 'dark'">☀️</span>
</button>
<button @click="setLanguage('en')" :class="lang === 'en' ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200'" class="px-3 py-1 rounded text-sm">
English
</button>
<button @click="setLanguage('zh')" :class="lang === 'zh' ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200'" class="px-3 py-1 rounded text-sm">
中文
</button>
</div>
</header>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2" x-text="t('status')"></h3>
<p class="text-2xl font-bold" :class="status.status === 'running' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'" x-text="t('status_' + status.status)"></p>
</div>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2" x-text="t('indexed_projects')"></h3>
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400" x-text="status.project_count"></p>
</div>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2" x-text="t('storage_path')"></h3>
<p class="text-sm text-gray-600 dark:text-gray-300 truncate" x-text="status.storage_path"></p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100" x-text="t('configuration')"></h2>
<button @click="editMode = !editMode" class="px-3 py-1 bg-blue-500 dark:bg-blue-600 text-white rounded hover:bg-blue-600 dark:hover:bg-blue-700 text-sm">
<span x-text="editMode ? t('cancel') : t('edit')"></span>
</button>
</div>
<div x-show="!editMode" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('base_url')"></label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100" x-text="config.base_url"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('token')"></label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100" x-text="config.token"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('batch_size')"></label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100" x-text="config.batch_size"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('max_lines_per_blob')"></label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100" x-text="config.max_lines_per_blob"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('text_extensions')"></label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100" x-text="config.text_extensions?.join(', ')"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('exclude_patterns')"></label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100" x-text="config.exclude_patterns?.join(', ')"></p>
</div>
</div>
<form x-show="editMode" @submit.prevent="saveConfig()" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('base_url')"></label>
<input type="text" x-model="editConfig.base_url" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('token')"></label>
<input type="text" x-model="editConfig.token" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('batch_size')"></label>
<input type="number" x-model.number="editConfig.batch_size" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('max_lines_per_blob')"></label>
<input type="number" x-model.number="editConfig.max_lines_per_blob" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('max_lines_per_blob_hint')"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('text_extensions')"></label>
<textarea x-model="editConfig.text_extensions_str" rows="3" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border" placeholder=".py, .js, .ts, .md"></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('text_extensions_hint')"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('exclude_patterns')"></label>
<textarea x-model="editConfig.exclude_patterns_str" rows="4" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border" placeholder=".venv, node_modules, .git"></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('exclude_patterns_hint')"></p>
</div>
<div class="flex gap-2">
<button type="submit" class="px-4 py-2 bg-green-500 dark:bg-green-600 text-white rounded hover:bg-green-600 dark:hover:bg-green-700 text-sm" x-text="t('save_changes')">
</button>
<button type="button" @click="editMode = false" class="px-4 py-2 bg-gray-500 dark:bg-gray-600 text-white rounded hover:bg-gray-600 dark:hover:bg-gray-700 text-sm" x-text="t('cancel')">
</button>
</div>
<p x-show="saveMessage" x-text="saveMessage" class="text-sm" :class="saveSuccess ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"></p>
</form>
</div>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100" x-text="t('realtime_logs')"></h2>
<button @click="clearLogs()" class="px-3 py-1 bg-red-500 dark:bg-red-600 text-white rounded hover:bg-red-600 dark:hover:bg-red-700 text-sm" x-text="t('clear')">
</button>
</div>
<div class="bg-gray-900 dark:bg-black text-green-400 dark:text-green-300 p-4 rounded font-mono text-xs h-96 overflow-y-auto" id="log-container">
<template x-for="log in logs" :key="log.id">
<div x-text="log.text" class="mb-1"></div>
</template>
<template x-if="logs.length === 0">
<div class="text-gray-500 dark:text-gray-400" x-text="t('waiting_logs')"></div>
</template>
</div>
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<span :class="wsConnected ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
● <span x-text="wsConnected ? t('connected') : t('disconnected')"></span>
</span>
</div>
</div>
</div>
<!-- Indexed Projects Section -->
<div class="mt-6 bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100" x-text="t('indexed_projects_list')"></h2>
<button @click="loadProjects()" class="px-3 py-1 bg-blue-500 dark:bg-blue-600 text-white rounded hover:bg-blue-600 dark:hover:bg-blue-700 text-sm">
<span x-text="t('refresh')"></span>
</button>
</div>
<div x-show="projects.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
<p x-text="t('no_indexed_projects')"></p>
</div>
<div x-show="projects.length > 0" class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider" x-text="t('project_path')"></th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider" x-text="t('blob_count')"></th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider" x-text="t('actions')"></th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="project in projects" :key="project.path">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-100 font-mono truncate max-w-md" :title="project.path" x-text="project.path"></td>
<td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-100">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200" x-text="project.blob_count"></span>
</td>
<td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-100 space-x-2">
<button @click="copyToToolInput(project.path)" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300" x-text="t('use_in_tool')"></button>
<button @click="viewProjectDetails(project.path)" class="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300" x-text="t('view_details')"></button>
<button @click="reindexProject(project.path)" class="text-orange-600 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-300" x-text="t('reindex')"></button>
<button @click="deleteProject(project.path)" class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300" x-text="t('delete')"></button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Project Details Modal -->
<div x-show="showProjectDetails" x-cloak class="fixed inset-0 bg-gray-600 dark:bg-gray-900 bg-opacity-50 dark:bg-opacity-70 overflow-y-auto h-full w-full z-50" @click.self="showProjectDetails = false">
<div class="relative top-20 mx-auto p-5 border border-gray-200 dark:border-gray-700 w-11/12 max-w-4xl shadow-lg rounded-md bg-white dark:bg-gray-800">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100" x-text="t('project_details')"></h3>
<button @click="showProjectDetails = false" class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('project_path')"></label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100 font-mono break-all" x-text="projectDetails.path"></p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('blob_count')"></label>
<p class="mt-1 text-2xl font-bold text-blue-600 dark:text-blue-400" x-text="projectDetails.blob_count"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('file_count')"></label>
<p class="mt-1 text-2xl font-bold text-green-600 dark:text-green-400" x-text="projectDetails.file_count || 0"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('file_types')"></label>
<p class="mt-1 text-2xl font-bold text-purple-600 dark:text-purple-400" x-text="Object.keys(projectDetails.file_type_stats || {}).length"></p>
</div>
</div>
<div x-show="Object.keys(projectDetails.file_type_stats || {}).length > 0">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" x-text="t('file_type_distribution')"></label>
<div class="space-y-2">
<template x-for="[ext, count] in Object.entries(projectDetails.file_type_stats || {})" :key="ext">
<div class="flex items-center">
<span class="w-20 text-sm text-gray-600 dark:text-gray-300 font-mono" x-text="ext"></span>
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-4 mx-3">
<div class="bg-blue-600 dark:bg-blue-500 h-4 rounded-full transition-all" :style="`width: ${(count / (projectDetails.file_count || 1) * 100).toFixed(1)}%`"></div>
</div>
<span class="w-16 text-sm text-gray-900 dark:text-gray-100 text-right" x-text="count"></span>
<span class="w-16 text-sm text-gray-500 dark:text-gray-400 text-right" x-text="`${(count / (projectDetails.file_count || 1) * 100).toFixed(1)}%`"></span>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
<!-- Tool Debugger Section -->
<div class="mt-6 bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4" x-text="t('tool_debugger')"></h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Tool Selection and Input -->
<div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" x-text="t('select_tool')"></label>
<select x-model="selectedTool" @change="updateToolForm()" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border">
<option value="search_context" x-text="t('tool_search_context')"></option>
</select>
</div>
<!-- Search History -->
<div x-show="searchHistory.length > 0" class="mb-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('search_history')"></label>
<button type="button" @click="clearSearchHistory()" class="text-xs text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300" x-text="t('clear_history')"></button>
</div>
<div class="space-y-1 max-h-32 overflow-y-auto">
<template x-for="(item, index) in searchHistory.slice(0, 10)" :key="index">
<div class="flex items-center gap-2 text-sm">
<button type="button" @click="useSearchHistory(item)" class="flex-1 text-left px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded truncate">
<span class="text-gray-600 dark:text-gray-400">🔍</span>
<span class="text-gray-900 dark:text-gray-100 ml-1" x-text="item.query"></span>
</button>
<span class="text-xs text-gray-400 dark:text-gray-500" x-text="formatDate(item.timestamp)"></span>
</div>
</template>
</div>
</div>
<form @submit.prevent="executeTool()" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" x-text="t('project_root_path')"></label>
<div class="flex gap-2">
<input type="text" x-model="toolArgs.project_root_path" class="flex-1 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border" placeholder="C:/Users/username/projects/myproject" required>
<button type="button" @click="checkProjectStatus()" class="px-3 py-2 bg-green-500 dark:bg-green-600 text-white rounded hover:bg-green-600 dark:hover:bg-green-700 text-sm whitespace-nowrap" x-text="t('check_index')"></button>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('project_root_path_hint')"></p>
<!-- 项目索引状态显示 -->
<div x-show="projectStatus.checked" class="mt-2 p-3 rounded text-sm" :class="projectStatus.indexed ? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800' : 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800'">
<div class="flex items-center gap-2">
<span x-show="projectStatus.indexed" class="text-green-600 dark:text-green-400">✅</span>
<span x-show="!projectStatus.indexed" class="text-yellow-600 dark:text-yellow-400">⚠️</span>
<span class="text-gray-900 dark:text-gray-100" x-text="projectStatus.indexed ? t('project_indexed') : t('project_not_indexed')"></span>
</div>
<div x-show="projectStatus.indexed" class="mt-1 text-xs text-gray-600 dark:text-gray-400">
<span x-text="t('blob_count')"></span>: <span x-text="projectStatus.blob_count"></span>
</div>
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('normalized_path') + ': ' + projectStatus.normalized_path"></div>
</div>
</div>
<div x-show="selectedTool === 'search_context'">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" x-text="t('query')"></label>
<textarea x-model="toolArgs.query" rows="3" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm p-2 border" placeholder="查找所有调用 get_model 的地方"></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('query_hint')"></p>
</div>
<div class="flex gap-2">
<button type="submit" :disabled="toolExecuting" class="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded hover:bg-blue-600 dark:hover:bg-blue-700 text-sm disabled:bg-gray-400 dark:disabled:bg-gray-600 disabled:cursor-not-allowed">
<span x-show="!toolExecuting" x-text="t('execute_tool')"></span>
<span x-show="toolExecuting" x-text="t('executing')"></span>
</button>
<button type="button" @click="clearToolResult()" class="px-4 py-2 bg-gray-500 dark:bg-gray-600 text-white rounded hover:bg-gray-600 dark:hover:bg-gray-700 text-sm" x-text="t('clear')">
</button>
</div>
</form>
</div>
<!-- Tool Result -->
<div>
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" x-text="t('tool_result')"></label>
<div x-show="toolResult && !toolError" class="space-x-2">
<button type="button" @click="exportAsMarkdown()" class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300" x-text="t('export_markdown')"></button>
<button type="button" @click="exportAsJSON()" class="text-xs text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300" x-text="t('export_json')"></button>
<button type="button" @click="copyToClipboard()" class="text-xs text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-300" x-text="t('copy_to_clipboard')"></button>
</div>
</div>
<div class="bg-gray-900 dark:bg-black text-green-400 dark:text-green-300 p-4 rounded font-mono text-xs h-96 overflow-y-auto whitespace-pre-wrap" x-html="toolResult || '<span class="text-gray-500 dark:text-gray-400">' + t('no_result') + '</span>'">
</div>
<div x-show="toolError" class="mt-2 text-sm text-red-600 dark:text-red-400" x-text="toolError"></div>
</div>
</div>
</div>
</div>
<script>
const translations = {
en: {
title: 'Acemcp Management',
subtitle: 'MCP Server for Codebase Indexing',
status: 'Status',
status_running: 'Running',
status_loading: 'Loading',
indexed_projects: 'Indexed Projects',
storage_path: 'Storage Path',
check_index: 'Check',
project_indexed: 'Project is indexed',
project_not_indexed: 'Project not indexed yet',
blob_count: 'Blobs',
normalized_path: 'Normalized path',
indexed_projects_list: 'Indexed Projects',
refresh: 'Refresh',
no_indexed_projects: 'No indexed projects yet',
project_path: 'Project Path',
actions: 'Actions',
use_in_tool: 'Use in Tool',
view_details: 'Details',
reindex: 'Reindex',
delete: 'Delete',
project_details: 'Project Details',
file_types: 'File Types',
types: 'types',
file_type_distribution: 'File Type Distribution',
confirm_delete: 'Are you sure you want to delete this project index?',
confirm_reindex: 'Are you sure you want to reindex this project?',
reindexing: 'Reindexing...',
deleting: 'Deleting...',
search_history: 'Search History',
clear_history: 'Clear',
export_markdown: 'Markdown',
export_json: 'JSON',
copy_to_clipboard: 'Copy',
exported: 'Exported!',
copied: 'Copied to clipboard!',
configuration: 'Configuration',
edit: 'Edit',
cancel: 'Cancel',
base_url: 'Base URL',
token: 'Token',
batch_size: 'Batch Size',
max_lines_per_blob: 'Max Lines Per Blob',
max_lines_per_blob_hint: 'Maximum lines per blob before splitting (default: 800)',
text_extensions: 'Text Extensions',
text_extensions_hint: 'Comma-separated list of file extensions (e.g., .py, .js, .ts)',
exclude_patterns: 'Exclude Patterns',
exclude_patterns_hint: 'Comma-separated patterns to exclude (e.g., .venv, node_modules, *.pyc)',
save_changes: 'Save Changes',
realtime_logs: 'Real-time Logs',
clear: 'Clear',
waiting_logs: 'Waiting for logs...',
connected: 'Connected',
disconnected: 'Disconnected',
tool_debugger: 'Tool Debugger',
select_tool: 'Select Tool',
tool_search_context: 'Search Context (Auto-indexes before search)',
project_root_path: 'Project Root Path',
project_root_path_hint: 'Use forward slashes (/) as path separators',
query: 'Query',
query_hint: 'Use descriptive keywords for semantic search. Example: "查找所有调用 get_model 的地方"',
execute_tool: 'Execute Tool',
executing: 'Executing...',
tool_result: 'Tool Result',
no_result: 'No result yet. Execute a tool to see the output.',
is_required: 'is required'
},
zh: {
title: 'Acemcp 管理',
subtitle: '代码库索引 MCP 服务器',
status: '状态',
status_running: '运行中',
status_loading: '加载中',
indexed_projects: '已索引项目',
storage_path: '存储路径',
check_index: '检查',
project_indexed: '项目已索引',
project_not_indexed: '项目尚未索引',
blob_count: 'Blob 数量',
normalized_path: '规范化路径',
indexed_projects_list: '已索引项目',
refresh: '刷新',
no_indexed_projects: '暂无已索引项目',
project_path: '项目路径',
actions: '操作',
use_in_tool: '在工具中使用',
view_details: '详情',
reindex: '重新索引',
delete: '删除',
project_details: '项目详情',
file_types: '文件类型',
types: '种',
file_type_distribution: '文件类型分布',
confirm_delete: '确定要删除此项目索引吗?',
confirm_reindex: '确定要重新索引此项目吗?',
reindexing: '正在重新索引...',
deleting: '正在删除...',
search_history: '搜索历史',
clear_history: '清空',
export_markdown: 'Markdown',
export_json: 'JSON',
copy_to_clipboard: '复制',
exported: '已导出!',
copied: '已复制到剪贴板!',
configuration: '配置',
edit: '编辑',
cancel: '取消',
base_url: '基础 URL',
token: '令牌',
batch_size: '批次大小',
max_lines_per_blob: '每个 Blob 最大行数',
max_lines_per_blob_hint: '文件拆分前的最大行数(默认:800)',
text_extensions: '文本扩展名',
text_extensions_hint: '逗号分隔的文件扩展名列表(例如:.py, .js, .ts)',
exclude_patterns: '排除模式',
exclude_patterns_hint: '逗号分隔的排除模式(例如:.venv, node_modules, *.pyc)',
save_changes: '保存更改',
realtime_logs: '实时日志',
clear: '清空',
waiting_logs: '等待日志...',
connected: '已连接',
disconnected: '已断开',
tool_debugger: '工具调试器',
select_tool: '选择工具',
tool_search_context: '搜索上下文(搜索前自动索引)',
project_root_path: '项目根路径',
project_root_path_hint: '使用正斜杠 (/) 作为路径分隔符',
query: '查询',
query_hint: '使用描述性关键词进行语义搜索。示例:"查找所有调用 get_model 的地方"',
execute_tool: '执行工具',
executing: '执行中...',
tool_result: '工具结果',
no_result: '暂无结果。执行工具以查看输出。',
is_required: '为必填项'
}
};
function mcpManager() {
return {
lang: localStorage.getItem('lang') || 'zh',
theme: localStorage.getItem('theme') || 'light',
status: {
status: 'loading',
project_count: 0,
storage_path: ''
},
config: {},
editConfig: {},
editMode: false,
saveMessage: '',
saveSuccess: false,
logs: [],
wsConnected: false,
ws: null,
logIdCounter: 0,
wsReconnectAttempts: 0,
wsMaxReconnectAttempts: 10,
wsReconnectDelay: 1000,
wsReconnectTimer: null,
wsManualClose: false,
selectedTool: 'search_context',
toolArgs: {
project_root_path: '',
query: ''
},
toolResult: '',
toolError: '',
toolExecuting: false,
projectStatus: {
checked: false,
indexed: false,
blob_count: 0,
normalized_path: ''
},
projects: [],
showProjectDetails: false,
projectDetails: {
path: '',
blob_count: 0,
file_type_stats: {}
},
searchHistory: [],
t(key) {
return translations[this.lang][key] || key;
},
setLanguage(lang) {
this.lang = lang;
localStorage.setItem('lang', lang);
},
async init() {
this.applyTheme();
await this.loadStatus();
await this.loadConfig();
await this.loadProjects();
this.loadSearchHistory();
this.connectWebSocket();
setInterval(() => this.loadStatus(), 5000);
},
applyTheme() {
if (this.theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
},
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', this.theme);
this.applyTheme();
},
loadSearchHistory() {
try {
const history = localStorage.getItem('search_history');
if (history) {
this.searchHistory = JSON.parse(history);
}
} catch (error) {
console.error('Failed to load search history:', error);
this.searchHistory = [];
}
},
saveSearchToHistory(projectPath, query) {
const historyItem = {
project_path: projectPath,
query: query,
timestamp: new Date().toISOString()
};
// 添加到历史记录开头
this.searchHistory.unshift(historyItem);
// 限制历史记录数量为 50
if (this.searchHistory.length > 50) {
this.searchHistory = this.searchHistory.slice(0, 50);
}
// 保存到 localStorage
try {
localStorage.setItem('search_history', JSON.stringify(this.searchHistory));
} catch (error) {
console.error('Failed to save search history:', error);
}
},
useSearchHistory(item) {
this.toolArgs.project_root_path = item.project_path;
this.toolArgs.query = item.query;
},
clearSearchHistory() {
if (confirm(this.t('confirm_delete'))) {
this.searchHistory = [];
localStorage.removeItem('search_history');
}
},
formatDate(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return this.lang === 'zh' ? '刚刚' : 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}${this.lang === 'zh' ? '分钟前' : 'm ago'}`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}${this.lang === 'zh' ? '小时前' : 'h ago'}`;
return date.toLocaleDateString();
},
async loadStatus() {
try {
const response = await fetch('/api/status');
this.status = await response.json();
} catch (error) {
console.error('Failed to load status:', error);
}
},
async loadConfig() {
try {
const response = await fetch('/api/config');
this.config = await response.json();
this.editConfig = {
base_url: this.config.base_url,
token: this.config.token_full || '',
batch_size: this.config.batch_size,
max_lines_per_blob: this.config.max_lines_per_blob,
text_extensions_str: this.config.text_extensions?.join(', ') || '',
exclude_patterns_str: this.config.exclude_patterns?.join(', ') || ''
};
} catch (error) {
console.error('Failed to load config:', error);
}
},
async loadProjects() {
try {
const response = await fetch('/api/projects');
const data = await response.json();
this.projects = data.projects || [];
} catch (error) {
console.error('Failed to load projects:', error);
this.projects = [];
}
},
copyToToolInput(projectPath) {
this.toolArgs.project_root_path = projectPath;
// 滚动到工具调试器区域
document.querySelector('[x-text="t(\'tool_debugger\')"]')?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
// 自动检查项目状态
setTimeout(() => this.checkProjectStatus(), 300);
},
async viewProjectDetails(projectPath) {
try {
const response = await fetch('/api/projects/details', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
project_path: projectPath
})
});
if (response.ok) {
this.projectDetails = await response.json();
this.showProjectDetails = true;
} else {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const error = await response.json();
alert('Error: ' + (error.error || error.message || 'Unknown error'));
} else {
const text = await response.text();
console.error('Non-JSON response:', text);
alert(`Error ${response.status}: ${response.statusText}`);
}
}
} catch (error) {
console.error('Failed to get project details:', error);
alert('Failed to get project details: ' + error.message);
}
},
async reindexProject(projectPath) {
if (!confirm(this.t('confirm_reindex'))) {
return;
}
let button = null;
let oldText = '';
try {
// 保存按钮状态
button = event?.target;
if (button) {
oldText = button.textContent;
button.textContent = this.t('reindexing');
button.disabled = true;
}
const response = await fetch('/api/projects/reindex', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
project_path: projectPath
})
});
if (response.ok) {
const result = await response.json();
alert(result.message || 'Project reindexed successfully');
await this.loadProjects();
} else {
// 检查响应类型
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const error = await response.json();
alert('Error: ' + (error.error || error.message || 'Unknown error'));
} else {
// 收到了HTML或其他非JSON响应
const text = await response.text();
console.error('Non-JSON response:', text);
alert(`Error ${response.status}: ${response.statusText}`);
}
}
} catch (error) {
console.error('Failed to reindex project:', error);
alert('Failed to reindex project: ' + error.message);
} finally {
// 恢复按钮状态
if (button && oldText) {
button.textContent = oldText;
button.disabled = false;
}
}
},
async deleteProject(projectPath) {
if (!confirm(this.t('confirm_delete'))) {
return;
}
let button = null;
let oldText = '';
try {
button = event?.target;
if (button) {
oldText = button.textContent;
button.textContent = this.t('deleting');
button.disabled = true;
}
const response = await fetch('/api/projects/delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
project_path: projectPath
})
});
if (response.ok) {
const result = await response.json();
alert(result.message);
await this.loadProjects();
} else {
// 检查响应类型
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const error = await response.json();
alert('Error: ' + (error.error || error.message || 'Unknown error'));
} else {
const text = await response.text();
console.error('Non-JSON response:', text);
alert(`Error ${response.status}: ${response.statusText}`);
}
}
} catch (error) {
console.error('Failed to delete project:', error);
alert('Failed to delete project: ' + error.message);
} finally {
if (button && oldText) {
button.textContent = oldText;
button.disabled = false;
}
}
},
async saveConfig() {
try {
this.saveMessage = 'Saving...';
this.saveSuccess = true;
const payload = {
base_url: this.editConfig.base_url,
token: this.editConfig.token,
batch_size: this.editConfig.batch_size,
max_lines_per_blob: this.editConfig.max_lines_per_blob,
text_extensions: this.editConfig.text_extensions_str
.split(',')
.map(ext => ext.trim())
.filter(ext => ext.length > 0),
exclude_patterns: this.editConfig.exclude_patterns_str
.split(',')
.map(pattern => pattern.trim())
.filter(pattern => pattern.length > 0)
};
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
const result = await response.json();
this.saveMessage = result.message;
this.saveSuccess = true;
await this.loadConfig();
await this.loadStatus();
setTimeout(() => {
this.editMode = false;
this.saveMessage = '';
}, 3000);
} else {
const error = await response.json();
this.saveMessage = error.detail || 'Failed to save configuration';
this.saveSuccess = false;
}
} catch (error) {
console.error('Failed to save config:', error);
this.saveMessage = 'Failed to save configuration: ' + error.message;
this.saveSuccess = false;
}
},
connectWebSocket() {
if (this.wsReconnectTimer) {
clearTimeout(this.wsReconnectTimer);
this.wsReconnectTimer = null;
}
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
console.log('Closing existing WebSocket connection');
this.wsManualClose = true;
this.ws.close();
}
if (this.wsReconnectAttempts >= this.wsMaxReconnectAttempts) {
console.warn('Max WebSocket reconnect attempts reached. Please refresh the page.');
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/logs`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.wsConnected = true;
this.wsReconnectAttempts = 0;
this.wsReconnectDelay = 1000;
this.wsManualClose = false;
console.log('WebSocket connected');
};
this.ws.onmessage = (event) => {
this.logs.push({
id: this.logIdCounter++,
text: event.data
});
if (this.logs.length > 500) {
this.logs.shift();
}
this.$nextTick(() => {
const container = document.getElementById('log-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
});
};
this.ws.onclose = (event) => {
this.wsConnected = false;
if (this.wsManualClose || event.code === 1000) {
console.log('WebSocket closed normally');
return;
}
this.wsReconnectAttempts++;
const delay = Math.min(this.wsReconnectDelay * Math.pow(1.5, this.wsReconnectAttempts - 1), 30000);
console.log(`WebSocket disconnected (attempt ${this.wsReconnectAttempts}/${this.wsMaxReconnectAttempts}), reconnecting in ${delay}ms...`);
this.wsReconnectTimer = setTimeout(() => this.connectWebSocket(), delay);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
},
disconnectWebSocket() {
this.wsManualClose = true;
if (this.wsReconnectTimer) {
clearTimeout(this.wsReconnectTimer);
this.wsReconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.wsConnected = false;
},
clearLogs() {
this.logs = [];
},
updateToolForm() {
if (this.selectedTool === 'index_code') {
this.toolArgs.query = '';
}
},
async checkProjectStatus() {
if (!this.toolArgs.project_root_path) {
alert(this.t('project_root_path') + ' ' + this.t('is_required'));
return;
}
try {
const response = await fetch('/api/projects/check', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
project_path: this.toolArgs.project_root_path
})
});
if (response.ok) {
const result = await response.json();
this.projectStatus = {
checked: true,
indexed: result.indexed,
blob_count: result.blob_count,
normalized_path: result.normalized_path
};
} else {
const error = await response.json();
alert('Error: ' + error.error);
}
} catch (error) {
console.error('Failed to check project status:', error);
alert('Failed to check project status: ' + error.message);
}
},
async executeTool() {
try {
this.toolExecuting = true;
this.toolError = '';
this.toolResult = '<span class="text-yellow-400">Executing...</span>';
const payload = {
tool_name: this.selectedTool,
arguments: {}
};
if (this.toolArgs.project_root_path) {
payload.arguments.project_root_path = this.toolArgs.project_root_path;
}
if (this.selectedTool === 'search_context' && this.toolArgs.query) {
payload.arguments.query = this.toolArgs.query;
}
const response = await fetch('/api/tools/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.status === 'success') {
const formattedResult = JSON.stringify(result.result, null, 2);
this.toolResult = this.escapeHtml(formattedResult);
// 保存搜索历史(仅针对 search_context)
if (this.selectedTool === 'search_context' && this.toolArgs.query && this.toolArgs.project_root_path) {
this.saveSearchToHistory(this.toolArgs.project_root_path, this.toolArgs.query);
}
} else {
this.toolError = result.message || 'Unknown error';
this.toolResult = '<span class="text-red-400">Error: ' + this.escapeHtml(result.message || 'Unknown error') + '</span>';
}
} catch (error) {
console.error('Failed to execute tool:', error);
this.toolError = error.message;
this.toolResult = '<span class="text-red-400">Error: ' + this.escapeHtml(error.message) + '</span>';
} finally {
this.toolExecuting = false;
}
},
clearToolResult() {
this.toolResult = '';
this.toolError = '';
},
exportAsMarkdown() {
try {
// 移除HTML标签,获取纯文本
const temp = document.createElement('div');
temp.innerHTML = this.toolResult;
const text = temp.textContent || temp.innerText || '';
// 生成Markdown格式
const markdown = `# Search Result\n\n**Project**: ${this.toolArgs.project_root_path}\n**Query**: ${this.toolArgs.query}\n**Date**: ${new Date().toLocaleString()}\n\n## Result\n\n\`\`\`\n${text}\n\`\`\`\n`;
// 下载文件
this.downloadFile('search_result.md', markdown);
alert(this.t('exported'));
} catch (error) {
console.error('Export as Markdown failed:', error);
alert('Export failed: ' + error.message);
}
},
exportAsJSON() {
try {
const temp = document.createElement('div');
temp.innerHTML = this.toolResult;
const text = temp.textContent || temp.innerText || '';
const jsonData = {
project_path: this.toolArgs.project_root_path,
query: this.toolArgs.query,
timestamp: new Date().toISOString(),
result: text
};
this.downloadFile('search_result.json', JSON.stringify(jsonData, null, 2));
alert(this.t('exported'));
} catch (error) {
console.error('Export as JSON failed:', error);
alert('Export failed: ' + error.message);
}
},
async copyToClipboard() {
try {
const temp = document.createElement('div');
temp.innerHTML = this.toolResult;
const text = temp.textContent || temp.innerText || '';
await navigator.clipboard.writeText(text);
alert(this.t('copied'));
} catch (error) {
console.error('Copy to clipboard failed:', error);
alert('Copy failed: ' + error.message);
}
},
downloadFile(filename, content) {
const blob = new Blob([content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
}
</script>
</body>
</html>