美化gui对预启动结果的显示;mcp 工具支持预启动及llm调用单独开关控制;预执行结果可以选择保存到记忆。

This commit is contained in:
guo zebin
2025-12-17 22:08:50 +08:00
parent 017ed36429
commit 745c3dc620
6 changed files with 181 additions and 99 deletions

View File

@@ -167,6 +167,8 @@ def _attach_prestart_metadata(server_id: int, tools: List[Dict[str, Any]]) -> Li
cfg = pre_map.get(name, {}) if name else {}
item["prestart"] = bool(cfg)
item["prestart_params"] = dict(cfg.get("params", {})) if isinstance(cfg, dict) else {}
item["include_history"] = cfg.get("include_history", True) if isinstance(cfg, dict) else True
item["allow_function_call"] = cfg.get("allow_function_call", False) if isinstance(cfg, dict) else False
enriched.append(item)
return enriched
@@ -852,6 +854,8 @@ def get_all_online_server_tools():
name = tool.get('name')
if not name:
continue
# Ensure server_id is included for filtering
tool['server_id'] = server_id
current = aggregated.get(name)
if not current or tool.get('last_checked', 0.0) >= current.get('last_checked', 0.0):
aggregated[name] = tool
@@ -1106,6 +1110,9 @@ def set_prestart_tool(server_id, tool_name):
data = request.json or {}
enabled = bool(data.get("enabled", False))
params = data.get("params", {}) or {}
include_history = bool(data.get("include_history", True))
allow_function_call = bool(data.get("allow_function_call", False))
if params and not isinstance(params, dict):
return jsonify({
"success": False,
@@ -1113,7 +1120,13 @@ def set_prestart_tool(server_id, tool_name):
}), 400
if enabled:
prestart_registry.set_prestart(server_id, tool_name, params if isinstance(params, dict) else {})
prestart_registry.set_prestart(
server_id,
tool_name,
params if isinstance(params, dict) else {},
include_history=include_history,
allow_function_call=allow_function_call
)
action = "启用"
else:
prestart_registry.remove_prestart(server_id, tool_name)
@@ -1132,6 +1145,8 @@ def set_prestart_tool(server_id, tool_name):
"message": f"工具 {tool_name}{action}预启动",
"prestart": enabled,
"prestart_params": params if isinstance(params, dict) else {},
"include_history": include_history if enabled else True,
"allow_function_call": allow_function_call if enabled else False,
"tools": tools
})
except Exception as e:
@@ -1163,7 +1178,8 @@ def list_runnable_prestart_tools():
available = {t.get("name"): t for t in snapshot or []}
for tool_name, cfg in tool_map.items():
entry = available.get(tool_name)
if not entry or not entry.get("enabled", True):
# 预启动工具只需要工具可用即可,不检查是否启用
if not entry:
continue
params = cfg.get("params", {}) if isinstance(cfg, dict) else {}
runnable.append({
@@ -1171,6 +1187,8 @@ def list_runnable_prestart_tools():
"server_name": server.get("name", f"Server {server_id}"),
"tool": tool_name,
"params": params if isinstance(params, dict) else {},
"include_history": cfg.get("include_history", True),
"allow_function_call": cfg.get("allow_function_call", False)
})
return jsonify({
"success": True,

View File

@@ -44,7 +44,13 @@ def _ensure_loaded() -> None:
if not name:
continue
params = cfg.get("params", {}) if isinstance(cfg, Mapping) else {}
normalized[str(name)] = {"params": params if isinstance(params, Mapping) else {}}
include_history = cfg.get("include_history", True) if isinstance(cfg, Mapping) else True
allow_function_call = cfg.get("allow_function_call", False) if isinstance(cfg, Mapping) else False
normalized[str(name)] = {
"params": params if isinstance(params, Mapping) else {},
"include_history": include_history,
"allow_function_call": allow_function_call
}
if normalized:
loaded[server_id] = normalized
_prestart = loaded
@@ -77,14 +83,18 @@ def get_server_map(server_id: int) -> Dict[str, Dict[str, Any]]:
return dict(_prestart.get(server_id, {}))
def set_prestart(server_id: int, tool_name: str, params: Dict[str, Any]) -> None:
"""Enable prestart for a tool with parameter template."""
def set_prestart(server_id: int, tool_name: str, params: Dict[str, Any], include_history: bool = True, allow_function_call: bool = False) -> None:
"""Enable prestart for a tool with parameter template and options."""
if not tool_name:
return
_ensure_loaded()
with _lock:
server_map = _prestart.setdefault(int(server_id), {})
server_map[str(tool_name)] = {"params": params or {}}
server_map[str(tool_name)] = {
"params": params or {},
"include_history": include_history,
"allow_function_call": allow_function_call
}
_save_locked()

View File

@@ -357,18 +357,32 @@
.tool-btn.enabled .tool-status-dot {
background-color: #67c23a;
}
.tool-btn.prestart {
/* 预启动工具 - 已启用状态(红色实心背景) */
.tool-btn.prestart.enabled {
border-color: #f56c6c !important;
background-color: #f56c6c !important;
color: #fff !important;
}
.tool-btn.prestart .tool-status-dot {
background-color: #fff !important;
.tool-btn.prestart.enabled .tool-status-dot {
background-color: #67c23a !important; /* 绿色点表示已启用 */
}
.tool-btn.prestart:hover {
.tool-btn.prestart.enabled:hover {
background-color: #e64545 !important;
border-color: #e64545 !important;
}
/* 预启动工具 - 未启用状态(红色边框,白色背景) */
.tool-btn.prestart:not(.enabled) {
border: 2px solid #f56c6c !important;
background-color: #fff !important;
color: #f56c6c !important;
}
.tool-btn.prestart:not(.enabled) .tool-status-dot {
background-color: #c0c4cc !important; /* 灰色点表示未启用 */
}
.tool-btn.prestart:not(.enabled):hover {
background-color: #fef0f0 !important;
border-color: #f56c6c !important;
}
.prestart-tag {
margin-left: auto;
padding: 2px 8px;
@@ -629,11 +643,15 @@
<label class="mcp-label">工具:</label>
<div class="mcp-info-value" id="prestartToolLabel">--</div>
</div>
<div class="prestart-hint">调用参数支持使用 {{question}} 占位符替换为用户问题。</div>
<div class="prestart-required" id="prestartRequired"></div>
<div id="prestartParamsWrapper">
<textarea class="mcp-textarea" id="prestartParamsInput" placeholder='例如: {"query": "{{question}}"}'></textarea>
</div>
<div class="prestart-hint">调用参数支持使用 {{question}} 占位符替换为用户问题。</div>
<div class="prestart-required" id="prestartRequired"></div>
<div id="prestartParamsWrapper">
<textarea class="mcp-textarea" id="prestartParamsInput" placeholder='例如: {"query": "{{question}}"}'></textarea>
</div>
<div class="mcp-form-item" style="margin-top: 10px;">
<label class="mcp-label" style="width: auto; margin-right: 10px;">结果保存到记忆:</label>
<input type="checkbox" id="prestartIncludeHistory" checked>
</div>
</div>
<div class="mcp-dialog-footer">
<button class="add-server-btn" onclick="savePrestartConfig(true)">保存预启动</button>
@@ -1422,7 +1440,8 @@
let toolEnabled = true; // 默认启用
let prestart = false;
let prestartParams = {};
let includeHistory = true;
if (typeof tool === 'object' && tool !== null) {
toolName = tool.name || '未知工具';
if (typeof tool.enabled !== 'undefined') {
@@ -1432,12 +1451,13 @@
if (tool.prestart_params && typeof tool.prestart_params === 'object') {
prestartParams = tool.prestart_params;
}
includeHistory = tool.include_history !== false;
} else if (typeof tool === 'string') {
toolName = tool;
} else {
toolName = '未知工具';
}
// 创建工具按钮
const toolBtn = document.createElement('div');
toolBtn.className = `tool-btn ${toolEnabled ? 'enabled' : ''} ${prestart ? 'prestart' : ''}`;
@@ -1446,20 +1466,21 @@
toolBtn.dataset.serverId = serverId;
toolBtn.dataset.enabled = toolEnabled;
toolBtn.dataset.prestartParams = JSON.stringify(prestartParams || {});
toolBtn.dataset.includeHistory = includeHistory;
const statusDot = document.createElement('span');
statusDot.className = 'tool-status-dot';
const nameSpan = document.createElement('span');
nameSpan.textContent = toolName;
const prestartTag = document.createElement('span');
prestartTag.className = `prestart-tag ${prestart ? '' : 'inactive'}`;
prestartTag.textContent = prestart ? '预启动' : '预启动?';
prestartTag.title = '配置预启动参数(支持 {{question}} 占位符)';
prestartTag.title = '配置预启动参数';
prestartTag.addEventListener('click', function(event) {
event.stopPropagation();
showPrestartDialog(serverId, serverName, toolName, prestartParams, tool.inputSchema || {});
showPrestartDialog(serverId, serverName, toolName, prestartParams, tool.inputSchema || {}, includeHistory);
});
// 添加点击事件
@@ -1487,54 +1508,59 @@
}
let prestartContext = { serverId: null, serverName: '', toolName: '', params: {} };
let prestartContext = { serverId: null, serverName: '', toolName: '', params: {}, includeHistory: true };
function showPrestartDialog(serverId, serverName, toolName, params, inputSchema) {
console.log('showPrestartDialog called with:', { serverId, serverName, toolName, params, inputSchema });
function showPrestartDialog(serverId, serverName, toolName, params, inputSchema, includeHistory) {
console.log('showPrestartDialog called with:', { serverId, serverName, toolName, params, inputSchema, includeHistory });
prestartContext = {
serverId,
serverName: serverName || '',
toolName,
params: params || {},
schema: inputSchema || {}
schema: inputSchema || {},
includeHistory: includeHistory !== false // default true
};
const dialog = document.getElementById('prestartDialog');
if (!dialog) return;
const serverLabel = document.getElementById('prestartServerLabel');
const toolLabel = document.getElementById('prestartToolLabel');
const input = document.getElementById('prestartParamsInput');
const requiredBox = document.getElementById('prestartRequired');
const paramsWrapper = document.getElementById('prestartParamsWrapper');
const schema = inputSchema || {};
const requiredList = Array.isArray(schema.required) ? schema.required : [];
const properties = schema.properties || {};
console.log('Schema properties:', properties, 'required:', requiredList);
const toolLabel = document.getElementById('prestartToolLabel');
const input = document.getElementById('prestartParamsInput');
const requiredBox = document.getElementById('prestartRequired');
const paramsWrapper = document.getElementById('prestartParamsWrapper');
const includeHistoryCheck = document.getElementById('prestartIncludeHistory');
if (includeHistoryCheck) includeHistoryCheck.checked = prestartContext.includeHistory;
const schema = inputSchema || {};
const requiredList = Array.isArray(schema.required) ? schema.required : [];
const properties = schema.properties || {};
console.log('Schema properties:', properties, 'required:', requiredList);
if (serverLabel) {
serverLabel.textContent = serverName || serverId;
}
if (toolLabel) {
toolLabel.textContent = toolName || '--';
}
if (requiredBox) {
if (requiredList.length === 0) {
requiredBox.textContent = '无必填参数';
} else {
}
if (requiredBox) {
if (requiredList.length === 0) {
requiredBox.textContent = '无必填参数';
} else {
const items = requiredList.map(key => {
const desc = properties[key]?.description || '';
return desc ? `${key}: ${desc}` : key;
});
requiredBox.textContent = `必填参数:${items.join('')}`;
}
}
if (paramsWrapper) {
paramsWrapper.style.display = requiredList.length === 0 ? 'none' : 'block';
}
const mergedParams = Object.assign({}, params || {});
const defaultByType = (t) => {
if (t === 'array') return [];
if (t === 'object') return {};
if (t === 'integer' || t === 'number') return 0;
requiredBox.textContent = `必填参数:${items.join('')}`;
}
}
if (paramsWrapper) {
paramsWrapper.style.display = requiredList.length === 0 ? 'none' : 'block';
}
const mergedParams = Object.assign({}, params || {});
const defaultByType = (t) => {
if (t === 'array') return [];
if (t === 'object') return {};
if (t === 'integer' || t === 'number') return 0;
if (t === 'boolean') return false;
return '';
};
@@ -1626,12 +1652,14 @@
}
}
const includeHistory = document.getElementById('prestartIncludeHistory').checked;
fetch(`/api/mcp/servers/${prestartContext.serverId}/tools/${encodeURIComponent(prestartContext.toolName)}/prestart`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(enable ? { enabled: true, params } : { enabled: false })
body: JSON.stringify(enable ? { enabled: true, params, include_history: includeHistory } : { enabled: false })
})
.then(resp => resp.json())
.then(data => {
@@ -1642,7 +1670,10 @@
hidePrestartDialog();
const btn = findToolButton(prestartContext.serverId, prestartContext.toolName);
if (btn) {
btn.dataset.prestartParams = JSON.stringify(params || {});
// Update dataset with new values from response
btn.dataset.prestartParams = JSON.stringify(data.prestart_params || {});
btn.dataset.includeHistory = data.include_history;
const tag = btn.querySelector('.prestart-tag');
if (enable) {
btn.classList.add('prestart');
@@ -1657,6 +1688,24 @@
tag.classList.add('inactive');
}
}
// Update onclick listener to use new values (needs recreation or just rely on dataset/fetching again?
// Recreating the onclick handler on the tag is tricky without removing old one.
// Simplest is to refresh tools for this card, or update the click handler via a closure if possible.
// But here we can just update the tag's click handler property if we stored it, or
// since we used addEventListener with anonymous function, we can't remove it easily.
// However, the `showPrestartDialog` is called with values from `tool` object in `onCardSelected`.
// We need to update that `tool` object reference or reload the tools list.
// Triggering a reload of the current card's tools is best.
const card = document.querySelector(`.mcp-card[data-server-id="${prestartContext.serverId}"]`);
if (card && card.classList.contains('selected')) {
// Re-trigger selection logic to refresh tools list
// onCardSelected(card); // But we don't have access to onCardSelected here easily?
// It is defined in global scope.
if (typeof onCardSelected === 'function') {
onCardSelected(card);
}
}
}
})
.catch(err => {

View File

@@ -547,21 +547,21 @@ html {
padding: 4px 8px;
margin-top: 8px;
margin-bottom: 6px;
background-color: #f0fff4;
background-color: #f7f7f7; /* 改为浅灰背景 */
border-radius: 4px;
border: 1px solid #a8e6cf;
border: 1px solid #dcdcdc; /* 改为灰色边框 */
user-select: none;
}
.prestart-toggle:hover {
background-color: #e0ffe8;
background-color: #eeeeee; /* 悬停加深 */
}
.prestart-arrow {
display: inline-block;
font-size: 10px;
margin-right: 6px;
color: #4caf50;
color: #666666; /* 改为暗灰色 */
transition: transform 0.2s ease;
}
@@ -571,13 +571,13 @@ html {
.prestart-label {
font-size: 12px;
color: #4caf50;
color: #666666; /* 改为暗灰色 */
font-weight: 500;
}
.prestart-content-inline {
background-color: #f5fff7;
border: 1px dashed #a8e6cf;
background-color: #fafafa; /* 改为灰白背景 */
border: 1px dashed #dcdcdc; /* 改为灰色虚线 */
border-radius: 4px;
padding: 8px 10px;
margin-bottom: 8px;

View File

@@ -714,13 +714,13 @@ unadoptText(id) {
let prestartContent = '';
// 解析 prestart 标签 - 使用贪婪匹配确保匹配到最后一个 </prestart>
// 同时支持多个 prestart 标签的情况
const prestartRegex = /<prestart>([\s\S]*)<\/prestart>/i;
// 同时支持多个 prestart 标签的情况,以及支持属性
const prestartRegex = /<prestart(?:[^>]*)>([\s\S]*)<\/prestart>/i;
const prestartMatch = mainContent.match(prestartRegex);
if (prestartMatch && prestartMatch[1]) {
prestartContent = this.trimThinkLines(prestartMatch[1]);
// 移除所有 prestart 标签及其内容
mainContent = mainContent.replace(/<prestart>[\s\S]*<\/prestart>/gi, '');
mainContent = mainContent.replace(/<prestart(?:[^>]*)>[\s\S]*<\/prestart>/gi, '');
}
// 先尝试匹配完整的 think 标签

View File

@@ -301,8 +301,8 @@ def _remove_think_from_text(text: str) -> str:
return re.sub(r'<think>[\s\S]*?</think>', '', text, flags=re.IGNORECASE).strip()
def _run_prestart_tools(user_question: str) -> str:
"""Call configured prestart MCP tools and return a summary string."""
def _run_prestart_tools(user_question: str) -> List[Dict[str, Any]]:
"""Call configured prestart MCP tools and return a list of result objects."""
try:
resp = requests.get(
"http://127.0.0.1:5010/api/mcp/prestart/runnable",
@@ -312,19 +312,21 @@ def _run_prestart_tools(user_question: str) -> str:
payload = resp.json()
except Exception as exc:
util.log(1, f"获取预启动工具列表失败: {exc}")
return ""
return []
tools = payload.get("prestart_tools") or []
if not tools:
return ""
return []
outputs: List[str] = []
results: List[Dict[str, Any]] = []
for item in tools:
server_id = item.get("server_id")
tool_name = item.get("tool")
if not server_id or not tool_name:
continue
params = item.get("params") or {}
include_history = item.get("include_history", True)
try:
filled_params = _apply_question_placeholder(params, user_question)
except Exception:
@@ -355,16 +357,16 @@ def _run_prestart_tools(user_question: str) -> str:
except Exception:
pass
# 格式化为 "工具名(参数): 返回内容"
outputs.append(f"{tool_name}{params_str}\n{output.strip()}")
formatted_output = f"{tool_name}{params_str}\n{output.strip()}"
results.append({
"text": formatted_output,
"include_history": include_history
})
else:
error_msg = data.get("error") or "未知错误"
util.log(1, f"预启动工具 {tool_name} 执行失败: {error_msg}")
if outputs:
return "\n\n".join(outputs)
return ""
return results
def _truncate_history(history: List[ToolResult], limit: int = 6) -> str:
@@ -1463,13 +1465,27 @@ def question(content, username, observation=None):
util.log(1, f"获取相关记忆时出错: {exc}")
prestart_context = ""
prestart_stream_text = ""
try:
prestart_context = _run_prestart_tools(content)
if prestart_context:
util.log(1, f"预启动工具输出 {len(prestart_context.splitlines())}")
prestart_results = _run_prestart_tools(content)
if prestart_results:
# 提示词用的上下文(纯文本)
prestart_context = "\n\n".join(r["text"] for r in prestart_results)
# 流式输出用的文本(带标签)
stream_parts = []
for r in prestart_results:
if r.get("include_history"):
stream_parts.append(f'<prestart keep="true">{r["text"]}</prestart>')
else:
stream_parts.append(f'<prestart>{r["text"]}</prestart>')
prestart_stream_text = "\n".join(stream_parts)
util.log(1, f"预启动工具输出 {len(prestart_results)}")
except Exception as exc:
util.log(1, f"预启动工具执行失败: {exc}")
prestart_context = f"- 预启动工具执行失败: {exc}"
prestart_stream_text = f"<prestart>{prestart_context}</prestart>"
# 获取当前时间
current_time = datetime.datetime.now().strftime("%Y年%m月%d%H:%M:%S")
@@ -1667,10 +1683,10 @@ def question(content, username, observation=None):
def send_prestart_content() -> None:
"""在LLM生成之前先发送预启动工具结果"""
nonlocal accumulated_text, full_response_text, is_first_sentence
if prestart_context and prestart_context.strip():
prestart_tag = f"<prestart>{prestart_context}</prestart>"
write_sentence(prestart_tag, force_first=is_first_sentence)
full_response_text += prestart_tag
if prestart_stream_text and prestart_stream_text.strip():
# prestart_stream_text 已经包含标签
write_sentence(prestart_stream_text, force_first=is_first_sentence)
full_response_text += prestart_stream_text
is_first_sentence = False
def run_workflow(tool_registry: Dict[str, WorkflowToolSpec]) -> bool:
@@ -2101,30 +2117,19 @@ def save_agent_memory():
util.log(1, f"保存代理记忆失败: {str(e)}")
def get_mcp_tools() -> List[Dict[str, Any]]:
"""
从共享缓存获取所有可用且已启用的MCP工具列表。
排除预启动工具因为预启动工具会在LLM推理前自动执行
不需要作为可调用工具提供给规划器。
"""
"""Fetch all available MCP tools from the registry."""
try:
tools = mcp_tool_registry.get_enabled_tools()
if not tools:
return []
# 过滤掉预启动工具
filtered_tools = []
for tool in tools:
if not tool:
continue
server_id = tool.get("server_id")
tool_name = tool.get("name")
if server_id is not None and tool_name:
if prestart_registry.is_prestart(server_id, tool_name):
continue # 跳过预启动工具
filtered_tools.append(tool)
return filtered_tools
resp = requests.get("http://127.0.0.1:5010/api/mcp/servers/online/tools", timeout=5)
if resp.status_code == 200:
data = resp.json()
raw_tools = data.get("tools") or []
# 只返回启用的工具预启动工具只要启用也可以被LLM调用
filtered = [tool for tool in raw_tools if tool.get("enabled", True)]
return filtered
except Exception as e:
util.log(1, f"获取工具列表出错:{e}")
util.log(1, f"Failed to fetch MCP tools: {e}")
return []
return []
if __name__ == "__main__":