Add files via upload

This commit is contained in:
公明
2026-05-07 16:57:17 +08:00
committed by GitHub
parent 7b9070f106
commit 699b9181e6
7 changed files with 395 additions and 29 deletions
+28
View File
@@ -3196,6 +3196,12 @@ header {
border-color: rgba(220, 53, 69, 0.3);
}
.status-chip.status-cancelled {
background: rgba(108, 117, 125, 0.12);
color: var(--text-secondary, #6c757d);
border-color: rgba(108, 117, 125, 0.35);
}
.status-chip.status-pending,
.status-chip.status-unknown {
background: rgba(255, 193, 7, 0.12);
@@ -3203,6 +3209,18 @@ header {
border-color: rgba(255, 193, 7, 0.3);
}
.detail-abort-hint {
font-size: 0.875rem;
opacity: 0.88;
margin: 0 0 10px;
line-height: 1.45;
}
.detail-abort-section .btn-monitor-abort {
border-color: rgba(253, 126, 20, 0.55);
color: #fd7e14;
}
.detail-code-card {
background: var(--bg-secondary);
border: 1px dashed rgba(0, 0, 0, 0.06);
@@ -5517,6 +5535,16 @@ header {
color: var(--error-color);
}
.monitor-status-chip.cancelled {
background: rgba(108, 117, 125, 0.15);
color: var(--text-muted, #6c757d);
}
.monitor-execution-actions .btn-monitor-abort {
border-color: rgba(253, 126, 20, 0.55);
color: #fd7e14;
}
.monitor-execution-actions {
display: flex;
align-items: center;
+26
View File
@@ -394,6 +394,16 @@
"tasks": {
"title": "Task Management",
"stopTask": "Stop task",
"interruptModalTitle": "Interrupt current step",
"interruptReasonLabel": "Interrupt note",
"interruptModalHint": "Your note is saved as a user message and the agent continues in the same stream. Use \"Stop completely\" to end the task.",
"interruptReasonPlaceholder": "e.g. Tool is too slow—skip and summarize…",
"interruptReasonRequired": "Please enter a short note so the model can continue accordingly.",
"interruptSubmitting": "Submitting...",
"interruptConfirmContinue": "Interrupt & continue",
"interruptHardStop": "Stop completely",
"interruptModalClose": "Close",
"userInterruptTimelineTitle": "User interrupt note (continuing)",
"collapseDetail": "Collapse details",
"newTask": "New task",
"autoRefresh": "Auto refresh",
@@ -1260,6 +1270,8 @@
"statusCompleted": "Completed",
"statusRunning": "Running",
"statusFailed": "Failed",
"statusCancelled": "Cancelled",
"terminateExecution": "Stop",
"loading": "Loading...",
"noStatsData": "No statistical data",
"noExecutions": "No execution records",
@@ -1727,8 +1739,22 @@
"statusRunning": "Running",
"statusCompleted": "Completed",
"statusFailed": "Failed",
"statusCancelled": "Cancelled",
"unknown": "Unknown",
"getDetailFailed": "Failed to get details",
"runningNoResponseYet": "No output yet; the tool may still be running. If it hangs, use \"Stop tool\" below to end this call only.",
"abortTitle": "Execution control",
"abortHint": "Stops only this tool call. The conversation / multi-step task continues (unlike stopping the whole task).",
"abortBtn": "Stop tool",
"abortConfirm": "Stop this tool call? The overall conversation or iterative task will not be cancelled.",
"abortSuccess": "Cancellation requested; status will update when the tool returns.",
"abortFailed": "Failed to stop tool",
"abortNoteModalTitle": "Stop tool with a note",
"abortNoteModalHint": "Optional: why you stopped or how the model should continue. The model sees any tool output first, then a labeled block (USER INTERRUPT NOTE — not raw tool output), then your text. Leave empty for a plain stop.",
"abortNoteLabel": "Note (optional)",
"abortNotePlaceholder": "e.g. Output is enough—skip waiting and continue…",
"abortNoteSubmit": "Stop tool",
"abortNoteClose": "Cancel",
"execSuccessNoContent": "Execution succeeded with no displayable content.",
"time": "Time",
"executionId": "Execution ID",
+26
View File
@@ -383,6 +383,16 @@
"tasks": {
"title": "任务管理",
"stopTask": "停止任务",
"interruptModalTitle": "中断当前步骤",
"interruptReasonLabel": "中断说明",
"interruptModalHint": "填写说明后将作为一条用户消息写入对话,智能体在同一会话内继续迭代。若只需完全停止任务,请点「彻底停止」。",
"interruptReasonPlaceholder": "例如:工具耗时过长,请先跳过并总结当前结果…",
"interruptReasonRequired": "请填写中断说明,以便模型根据你的意图继续。",
"interruptSubmitting": "提交中...",
"interruptConfirmContinue": "中断并继续",
"interruptHardStop": "彻底停止",
"interruptModalClose": "关闭",
"userInterruptTimelineTitle": "用户中断说明(继续迭代)",
"collapseDetail": "收起详情",
"newTask": "新建任务",
"autoRefresh": "自动刷新",
@@ -1249,6 +1259,8 @@
"statusCompleted": "已完成",
"statusRunning": "执行中",
"statusFailed": "失败",
"statusCancelled": "已终止",
"terminateExecution": "终止",
"loading": "加载中...",
"noStatsData": "暂无统计数据",
"noExecutions": "暂无执行记录",
@@ -1716,8 +1728,22 @@
"statusRunning": "执行中",
"statusCompleted": "已完成",
"statusFailed": "失败",
"statusCancelled": "已终止",
"unknown": "未知",
"getDetailFailed": "获取详情失败",
"runningNoResponseYet": "尚无返回,工具可能仍在执行。若长时间无响应,可使用下方「终止工具」结束本次调用。",
"abortTitle": "运行控制",
"abortHint": "仅中断当前这一次工具调用;对话与多步迭代任务会继续,不会等同于「停止任务」。",
"abortBtn": "终止工具",
"abortConfirm": "确定终止此次工具调用?整条对话或迭代任务不会因此停止。",
"abortSuccess": "已发送终止请求,工具返回后状态将更新。",
"abortFailed": "终止失败",
"abortNoteModalTitle": "终止工具并补充说明",
"abortNoteModalHint": "可选:说明为何终止或希望模型如何继续。提交后模型会先看到工具已输出内容(若有),再看到带「用户终止说明」标题的独立区块(中英标注,与命令行原文区分),最后是您的文字。留空则与原先仅终止一致。",
"abortNoteLabel": "终止说明(可选)",
"abortNotePlaceholder": "例如:输出已够判断,请停止等待并继续下一步…",
"abortNoteSubmit": "提交终止",
"abortNoteClose": "取消",
"execSuccessNoContent": "执行成功,未返回可展示的文本内容。",
"time": "时间",
"executionId": "执行 ID",
+5 -4
View File
@@ -306,12 +306,13 @@ async function bootstrapApp() {
// 通用工具函数
function getStatusText(status) {
const s = (status && String(status).toLowerCase()) || '';
if (typeof window.t !== 'function') {
const fallback = { pending: '等待中', running: '执行中', completed: '已完成', failed: '失败' };
return fallback[status] || status;
const fallback = { pending: '等待中', running: '执行中', completed: '已完成', failed: '失败', cancelled: '已终止' };
return fallback[s] || status;
}
const keyMap = { pending: 'mcpDetailModal.statusPending', running: 'mcpDetailModal.statusRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed' };
const key = keyMap[status];
const keyMap = { pending: 'mcpDetailModal.statusPending', running: 'mcpDetailModal.statusRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed', cancelled: 'mcpDetailModal.statusCancelled' };
const key = keyMap[s];
return key ? window.t(key) : status;
}
+113 -1
View File
@@ -2446,7 +2446,24 @@ async function showMCPDetail(executionId) {
}
}
} else {
responseElement.textContent = typeof window.t === 'function' ? window.t('chat.noResponseData') : '暂无响应数据';
if (normalizedStatus === 'running') {
responseElement.textContent = typeof window.t === 'function' ? window.t('mcpDetailModal.runningNoResponseYet') : '尚无返回,工具可能仍在执行。若长时间无响应,可在下方终止本次调用。';
} else {
responseElement.textContent = typeof window.t === 'function' ? window.t('chat.noResponseData') : '暂无响应数据';
}
}
const abortSection = document.getElementById('detail-abort-section');
const abortBtn = document.getElementById('detail-abort-btn');
if (abortSection && abortBtn) {
if (normalizedStatus === 'running') {
abortSection.style.display = 'block';
abortBtn.dataset.execId = exec.id || '';
abortBtn.textContent = typeof window.t === 'function' ? window.t('mcpDetailModal.abortBtn') : '终止工具';
} else {
abortSection.style.display = 'none';
delete abortBtn.dataset.execId;
}
}
// 显示模态框
@@ -2464,6 +2481,101 @@ function closeMCPDetail() {
document.getElementById('mcp-detail-modal').style.display = 'none';
}
/** 从详情模态框触发:取消当前进行中的 MCP 工具调用 */
async function abortMCPToolExecutionFromDetail() {
const btn = document.getElementById('detail-abort-btn');
const id = btn && btn.dataset.execId;
if (!id) {
return;
}
await cancelMCPToolExecution(id, { refreshDetail: true });
}
/**
* 打开 MCP 工具终止弹窗说明会经服务端加上用户终止说明标题块后与工具输出合并给模型
* @param {string} executionId
* @param {{ refreshDetail?: boolean }} [options]
*/
function openMcpToolAbortModal(executionId, options = {}) {
window.__mcpToolAbortContext = { executionId: executionId, options: options || {} };
const ta = document.getElementById('mcp-tool-abort-note');
if (ta) {
ta.value = '';
}
const m = document.getElementById('mcp-tool-abort-modal');
if (m) {
m.style.display = 'block';
}
}
function closeMcpToolAbortModal() {
window.__mcpToolAbortContext = null;
const m = document.getElementById('mcp-tool-abort-modal');
if (m) {
m.style.display = 'none';
}
}
async function submitMcpToolAbortModal() {
const ctx = window.__mcpToolAbortContext;
if (!ctx || !ctx.executionId) {
closeMcpToolAbortModal();
return;
}
const note = (document.getElementById('mcp-tool-abort-note') && document.getElementById('mcp-tool-abort-note').value || '').trim();
const executionId = ctx.executionId;
const options = ctx.options || {};
closeMcpToolAbortModal();
await cancelMCPToolExecutionSubmit(executionId, note, options);
}
/**
* 提交终止请求body: { note }
* @param {string} executionId
* @param {string} userNote
* @param {{ refreshDetail?: boolean }} [options]
*/
async function cancelMCPToolExecutionSubmit(executionId, userNote, options = {}) {
if (!executionId) {
return;
}
try {
const res = await apiFetch(`/api/monitor/execution/${encodeURIComponent(executionId)}/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ note: userNote || '' }),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(body.error || body.message || res.statusText);
}
const okMsg = typeof window.t === 'function' ? window.t('mcpDetailModal.abortSuccess') : '已发送终止请求';
alert(okMsg);
if (options.refreshDetail && typeof showMCPDetail === 'function') {
await showMCPDetail(executionId);
}
if (typeof refreshMonitorPanel === 'function') {
const page = (typeof monitorState !== 'undefined' && monitorState.pagination && monitorState.pagination.page) ? monitorState.pagination.page : 1;
await refreshMonitorPanel(page);
}
} catch (e) {
const failMsg = typeof window.t === 'function' ? window.t('mcpDetailModal.abortFailed') : '终止失败';
alert(failMsg + ': ' + (e && e.message ? e.message : String(e)));
}
}
/**
* 取消单次 MCP 工具执行监控页终止弹出说明框后提交仅取消该次 tools/call不停止整条对话/迭代任务
* @param {string} executionId
* @param {{ refreshDetail?: boolean }} [options]
*/
async function cancelMCPToolExecution(executionId, options = {}) {
if (!executionId) {
return;
}
openMcpToolAbortModal(executionId, options);
}
// 复制详情面板中的内容
function copyDetailBlock(elementId, triggerBtn = null) {
const target = document.getElementById(elementId);
+146 -24
View File
@@ -1,4 +1,6 @@
const progressTaskState = new Map();
/** @type {{ progressId: string, conversationId: string } | null} */
let userInterruptModalPending = null;
let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
@@ -410,6 +412,128 @@ async function requestCancel(conversationId) {
return result;
}
/** 用户填写说明后中断当前步骤,由后端写入对话并继续同一条流式迭代 */
async function requestCancelWithContinue(conversationId, reason) {
const response = await apiFetch('/api/agent-loop/cancel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
conversationId,
reason: reason || '',
continueAfter: true,
}),
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || (typeof window.t === 'function' ? window.t('tasks.cancelFailed') : '取消失败'));
}
return result;
}
function openUserInterruptModal(progressId, conversationId) {
userInterruptModalPending = { progressId, conversationId };
const ta = document.getElementById('user-interrupt-reason');
if (ta) {
ta.value = '';
}
const m = document.getElementById('user-interrupt-modal');
if (m) {
m.style.display = 'block';
}
}
function closeUserInterruptModal() {
userInterruptModalPending = null;
const m = document.getElementById('user-interrupt-modal');
if (m) {
m.style.display = 'none';
}
}
async function submitUserInterruptContinue() {
if (!userInterruptModalPending) {
return;
}
const reason = (document.getElementById('user-interrupt-reason') && document.getElementById('user-interrupt-reason').value || '').trim();
if (!reason) {
alert(typeof window.t === 'function' ? window.t('tasks.interruptReasonRequired') : '请填写中断说明');
return;
}
const { progressId, conversationId } = userInterruptModalPending;
closeUserInterruptModal();
const stopBtn = document.getElementById(`${progressId}-stop-btn`);
try {
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.interruptSubmitting') : '提交中...';
}
await requestCancelWithContinue(conversationId, reason);
loadActiveTasks();
} catch (error) {
console.error('中断并继续失败:', error);
alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '操作失败') + ': ' + error.message);
} finally {
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务';
}
}
}
async function submitUserInterruptHardCancel() {
if (!userInterruptModalPending) {
return;
}
const { progressId } = userInterruptModalPending;
closeUserInterruptModal();
await performHardCancelProgressTask(progressId);
}
/** 彻底停止任务(原「停止任务」行为) */
async function performHardCancelProgressTask(progressId) {
const state = progressTaskState.get(progressId);
const stopBtn = document.getElementById(`${progressId}-stop-btn`);
if (!state || !state.conversationId) {
if (stopBtn) {
stopBtn.disabled = true;
setTimeout(() => {
stopBtn.disabled = false;
}, 1500);
}
alert(typeof window.t === 'function' ? window.t('tasks.taskInfoNotSynced') : '任务信息尚未同步,请稍后再试。');
return;
}
if (state.cancelling) {
return;
}
markProgressCancelling(progressId);
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...';
}
try {
await requestCancel(state.conversationId);
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message);
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务';
}
const currentState = progressTaskState.get(progressId);
if (currentState) {
currentState.cancelling = false;
}
}
}
function addProgressMessage() {
const messagesDiv = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
@@ -737,7 +861,7 @@ function toggleProcessDetails(progressId, assistantMessageId) {
}
}
// 停止当前进度对应的任务
// 停止当前进度:弹出「中断并说明 / 彻底停止」
async function cancelProgressTask(progressId) {
const state = progressTaskState.get(progressId);
const stopBtn = document.getElementById(`${progressId}-stop-btn`);
@@ -757,27 +881,7 @@ async function cancelProgressTask(progressId) {
return;
}
markProgressCancelling(progressId);
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...';
}
try {
await requestCancel(state.conversationId);
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message);
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务';
}
const currentState = progressTaskState.get(progressId);
if (currentState) {
currentState.cancelling = false;
}
}
openUserInterruptModal(progressId, state.conversationId);
}
// 将进度消息转换为可折叠的详情组件
@@ -1414,6 +1518,18 @@ function handleStreamEvent(event, progressElement, progressId,
break;
}
case 'user_interrupt_continue': {
const d = event.data || {};
const reason = (d.reason != null && String(d.reason).trim() !== '') ? String(d.reason).trim() : (event.message || '');
const timelineTitle = typeof window.t === 'function' ? window.t('tasks.userInterruptTimelineTitle') : '用户中断说明(继续迭代)';
addTimelineItem(timeline, 'user_interrupt', {
title: '✋ ' + timelineTitle,
message: reason,
data: d,
});
break;
}
case 'progress':
const progressTitle = document.querySelector(`#${progressId} .progress-title`);
if (progressTitle) {
@@ -2777,7 +2893,8 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
const viewDetailLabel = typeof window.t === 'function' ? window.t('mcpMonitor.viewDetail') : '查看详情';
const deleteLabel = typeof window.t === 'function' ? window.t('mcpMonitor.delete') : '删除';
const deleteExecTitle = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecTitle') : '删除此执行记录';
const statusKeyMap = { pending: 'statusPending', running: 'statusRunning', completed: 'statusCompleted', failed: 'statusFailed' };
const terminateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.terminateExecution') : '终止';
const statusKeyMap = { pending: 'statusPending', running: 'statusRunning', completed: 'statusCompleted', failed: 'statusFailed', cancelled: 'statusCancelled' };
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
const rows = executions
.map(exec => {
@@ -2788,7 +2905,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
const startTime = exec.startTime ? (new Date(exec.startTime).toLocaleString ? new Date(exec.startTime).toLocaleString(locale || 'en-US') : String(exec.startTime)) : unknownLabel;
const duration = formatExecutionDuration(exec.startTime, exec.endTime);
const toolName = escapeHtml(exec.toolName || unknownToolLabel);
const executionId = escapeHtml(exec.id || '');
const rawExecId = exec.id || '';
const executionId = escapeHtml(rawExecId);
const terminateBtn = status === 'running'
? `<button type="button" class="btn-secondary btn-monitor-abort" onclick="cancelMCPToolExecution('${rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')">${escapeHtml(terminateLabel)}</button>`
: '';
return `
<tr>
<td>
@@ -2801,6 +2922,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
<td>
<div class="monitor-execution-actions">
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">${escapeHtml(viewDetailLabel)}</button>
${terminateBtn}
<button class="btn-secondary btn-delete" onclick="deleteExecution('${executionId}')" title="${escapeHtml(deleteExecTitle)}">${escapeHtml(deleteLabel)}</button>
</div>
</td>
+51
View File
@@ -1053,6 +1053,7 @@
<option value="completed" data-i18n="mcpMonitor.statusCompleted">已完成</option>
<option value="running" data-i18n="mcpMonitor.statusRunning">执行中</option>
<option value="failed" data-i18n="mcpMonitor.statusFailed">失败</option>
<option value="cancelled" data-i18n="mcpMonitor.statusCancelled">已终止</option>
</select>
</label>
</div>
@@ -2449,6 +2450,13 @@
</div>
</div>
</div>
<div class="detail-section detail-abort-section" id="detail-abort-section" style="display: none;">
<div class="detail-section-header">
<h3 data-i18n="mcpDetailModal.abortTitle">运行控制</h3>
</div>
<p class="detail-abort-hint" data-i18n="mcpDetailModal.abortHint">仅中断当前工具调用;对话与多步任务会继续。</p>
<button type="button" class="btn-secondary btn-monitor-abort" id="detail-abort-btn" onclick="abortMCPToolExecutionFromDetail()">终止工具</button>
</div>
<div class="detail-section">
<div class="detail-section-header">
<h3 data-i18n="mcpDetailModal.requestParams">请求参数</h3>
@@ -2489,6 +2497,49 @@
</div>
</div>
<!-- 用户中断并说明(继续迭代) -->
<div id="user-interrupt-modal" class="modal">
<div class="modal-content" style="max-width: 520px;">
<div class="modal-header">
<h2 data-i18n="tasks.interruptModalTitle">中断当前步骤</h2>
<span class="modal-close" onclick="closeUserInterruptModal()">&times;</span>
</div>
<div class="modal-body">
<p class="detail-abort-hint" data-i18n="tasks.interruptModalHint">填写说明后将写入对话并由智能体继续迭代。</p>
<div class="form-group">
<label for="user-interrupt-reason"><span data-i18n="tasks.interruptReasonLabel">中断说明</span></label>
<textarea id="user-interrupt-reason" class="form-control" rows="4" data-i18n="tasks.interruptReasonPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
</div>
<div class="form-actions" style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;">
<button type="button" class="btn-secondary" onclick="closeUserInterruptModal()" data-i18n="tasks.interruptModalClose">关闭</button>
<button type="button" class="btn-secondary btn-delete" onclick="submitUserInterruptHardCancel()" data-i18n="tasks.interruptHardStop">彻底停止</button>
<button type="button" class="btn-primary" onclick="submitUserInterruptContinue()" data-i18n="tasks.interruptConfirmContinue">中断并继续</button>
</div>
</div>
</div>
</div>
<!-- MCP 工具终止:可填写给模型的说明 -->
<div id="mcp-tool-abort-modal" class="modal">
<div class="modal-content" style="max-width: 520px;">
<div class="modal-header">
<h2 data-i18n="mcpDetailModal.abortNoteModalTitle">终止工具并补充说明</h2>
<span class="modal-close" onclick="closeMcpToolAbortModal()">&times;</span>
</div>
<div class="modal-body">
<p class="detail-abort-hint" data-i18n="mcpDetailModal.abortNoteModalHint">可选说明。</p>
<div class="form-group">
<label for="mcp-tool-abort-note"><span data-i18n="mcpDetailModal.abortNoteLabel">终止说明(可选)</span></label>
<textarea id="mcp-tool-abort-note" class="form-control" rows="4" data-i18n="mcpDetailModal.abortNotePlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
</div>
<div class="form-actions" style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;">
<button type="button" class="btn-secondary" onclick="closeMcpToolAbortModal()" data-i18n="mcpDetailModal.abortNoteClose">取消</button>
<button type="button" class="btn-primary" onclick="submitMcpToolAbortModal()" data-i18n="mcpDetailModal.abortNoteSubmit">提交终止</button>
</div>
</div>
</div>
</div>
<!-- 外部MCP配置模态框 -->
<div id="external-mcp-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">