Add files via upload

This commit is contained in:
公明
2026-03-09 22:19:22 +08:00
committed by GitHub
parent 379486d36c
commit f26ee8e6e7
11 changed files with 990 additions and 200 deletions
+146 -61
View File
@@ -3,13 +3,74 @@
let apiSpec = null;
let currentToken = null;
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
function waitForI18n() {
return new Promise(function (resolve) {
if (window.t) return resolve();
var n = 0;
var iv = setInterval(function () {
if (window.t) { clearInterval(iv); resolve(); return; }
n++;
if (n >= 100) { clearInterval(iv); resolve(); }
}, 50);
});
}
// 从 OpenAPI spec 的 x-i18n-tags 构建 tag -> i18n key 映射(方案 A:后端提供键)
var apiSpecTagToKey = {};
function buildApiSpecTagToKey() {
apiSpecTagToKey = {};
if (!apiSpec || !apiSpec.paths) return;
Object.keys(apiSpec.paths).forEach(function (path) {
var pathItem = apiSpec.paths[path];
if (!pathItem || typeof pathItem !== 'object') return;
['get', 'post', 'put', 'delete', 'patch'].forEach(function (method) {
var op = pathItem[method];
if (!op || !op.tags || !op['x-i18n-tags']) return;
var tags = op.tags;
var keys = op['x-i18n-tags'];
for (var i = 0; i < tags.length && i < keys.length; i++) {
apiSpecTagToKey[tags[i]] = typeof keys[i] === 'string' ? keys[i] : keys[i];
}
});
});
}
function translateApiDocTag(tag) {
if (!tag) return tag;
var key = apiSpecTagToKey[tag];
return key ? _t('apiDocs.tags.' + key) : tag;
}
function translateApiDocSummaryFromOp(op) {
var key = op && op['x-i18n-summary'];
if (key) return _t('apiDocs.summary.' + key);
return op && op.summary ? op.summary : '';
}
function translateApiDocResponseDescFromResp(resp) {
if (!resp) return '';
var key = resp['x-i18n-description'];
if (key) return _t('apiDocs.response.' + key);
return resp.description || '';
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
await waitForI18n();
await loadToken();
await loadAPISpec();
if (apiSpec) {
renderAPIDocs();
}
document.addEventListener('languagechange', function () {
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(document);
}
if (apiSpec) {
renderAPIDocs();
}
});
});
// 加载token
@@ -43,22 +104,25 @@ async function loadAPISpec() {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 401) {
showError('需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。');
showError(_t('apiDocs.errorLoginRequired'));
return;
}
throw new Error('加载API规范失败: ' + response.status);
throw new Error(_t('apiDocs.errorLoadSpec') + response.status);
}
apiSpec = await response.json();
buildApiSpecTagToKey();
} catch (error) {
console.error('加载API规范失败:', error);
showError('加载API文档失败: ' + error.message);
showError(_t('apiDocs.errorLoadFailed') + error.message);
}
}
// 显示错误
function showError(message) {
const main = document.getElementById('api-docs-main');
const loadFailed = _t('apiDocs.loadFailed');
const backToLogin = _t('apiDocs.backToLogin');
main.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -66,10 +130,10 @@ function showError(message) {
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<h3>加载失败</h3>
<p>${message}</p>
<h3>${escapeHtml(loadFailed)}</h3>
<p>${escapeHtml(message)}</p>
<div style="margin-top: 16px;">
<a href="/" style="color: var(--accent-color); text-decoration: none;">返回首页登录</a>
<a href="/" style="color: var(--accent-color); text-decoration: none;">${escapeHtml(backToLogin)}</a>
</div>
</div>
`;
@@ -78,7 +142,7 @@ function showError(message) {
// 渲染API文档
function renderAPIDocs() {
if (!apiSpec || !apiSpec.paths) {
showError('API规范格式错误');
showError(_t('apiDocs.errorSpecInvalid'));
return;
}
@@ -109,7 +173,7 @@ function renderAuthInfo() {
tokenStatus.style.display = 'block';
tokenStatus.style.background = 'rgba(255, 152, 0, 0.1)';
tokenStatus.style.borderLeftColor = '#ff9800';
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;"><strong>⚠ 未检测到 Token</strong> - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token</p>';
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;">' + escapeHtml(_t('apiDocs.tokenNotDetected')) + '</p>';
}
}
@@ -127,11 +191,14 @@ function renderSidebar() {
const groupList = document.getElementById('api-group-list');
const allGroups = Array.from(groups).sort();
while (groupList.children.length > 1) {
groupList.removeChild(groupList.lastChild);
}
allGroups.forEach(group => {
const li = document.createElement('li');
li.className = 'api-group-item';
li.innerHTML = `<a href="#" class="api-group-link" data-group="${group}">${group}</a>`;
const groupLabel = translateApiDocTag(group);
li.innerHTML = `<a href="#" class="api-group-link" data-group="${escapeHtml(group)}">${escapeHtml(groupLabel)}</a>`;
groupList.appendChild(li);
});
@@ -176,7 +243,7 @@ function renderEndpoints(filterGroup = null) {
});
if (endpoints.length === 0) {
main.innerHTML = '<div class="empty-state"><h3>暂无API</h3><p>该分组下没有API端点</p></div>';
main.innerHTML = '<div class="empty-state"><h3>' + escapeHtml(_t('apiDocs.noApis')) + '</h3><p>' + escapeHtml(_t('apiDocs.noEndpointsInGroup')) + '</p></div>';
return;
}
@@ -192,8 +259,8 @@ function createEndpointCard(endpoint) {
const methodClass = endpoint.method.toLowerCase();
const tags = endpoint.tags || [];
const tagHtml = tags.map(tag => `<span class="api-tag">${tag}</span>`).join('');
const tagHtml = tags.map(tag => `<span class="api-tag">${escapeHtml(translateApiDocTag(tag))}</span>`).join('');
const summaryText = translateApiDocSummaryFromOp(endpoint);
card.innerHTML = `
<div class="api-endpoint-header">
<div class="api-endpoint-title">
@@ -204,21 +271,21 @@ function createEndpointCard(endpoint) {
</div>
<div class="api-endpoint-body">
<div class="api-section">
<div class="api-section-title">描述</div>
${endpoint.summary ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(endpoint.summary)}</div>` : ''}
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionDescription'))}</div>
${summaryText ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(summaryText)}</div>` : ''}
${endpoint.description ? `
<div class="api-description-toggle">
<button class="description-toggle-btn" onclick="toggleDescription(this)">
<svg class="description-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
<span>查看详细说明</span>
<span>${escapeHtml(_t('apiDocs.viewDetailDesc'))}</span>
</button>
<div class="api-description-detail" style="display: none;">
${formatDescription(endpoint.description)}
</div>
</div>
` : endpoint.summary ? '' : '<div class="api-description">无描述</div>'}
` : endpoint.summary ? '' : '<div class="api-description">' + escapeHtml(_t('apiDocs.noDescription')) + '</div>'}
</div>
${renderParameters(endpoint)}
@@ -236,8 +303,10 @@ function renderParameters(endpoint) {
const params = endpoint.parameters || [];
if (params.length === 0) return '';
const requiredLabel = escapeHtml(_t('apiDocs.required'));
const optionalLabel = escapeHtml(_t('apiDocs.optional'));
const rows = params.map(param => {
const required = param.required ? '<span class="api-param-required">必需</span>' : '<span class="api-param-optional">可选</span>';
const required = param.required ? '<span class="api-param-required">' + requiredLabel + '</span>' : '<span class="api-param-optional">' + optionalLabel + '</span>';
// 处理描述文本,将换行符转换为<br>
let descriptionHtml = '-';
if (param.description) {
@@ -255,17 +324,20 @@ function renderParameters(endpoint) {
`;
}).join('');
const paramName = escapeHtml(_t('apiDocs.paramName'));
const typeLabel = escapeHtml(_t('apiDocs.type'));
const descLabel = escapeHtml(_t('apiDocs.description'));
return `
<div class="api-section">
<div class="api-section-title">参数</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionParams'))}</div>
<div class="api-table-wrapper">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
<th>${paramName}</th>
<th>${typeLabel}</th>
<th>${descLabel}</th>
<th>${requiredLabel}</th>
</tr>
</thead>
<tbody>
@@ -297,11 +369,13 @@ function renderRequestBody(endpoint) {
let paramsTable = '';
if (schema.properties) {
const requiredFields = schema.required || [];
const reqLabel = escapeHtml(_t('apiDocs.required'));
const optLabel = escapeHtml(_t('apiDocs.optional'));
const rows = Object.keys(schema.properties).map(key => {
const prop = schema.properties[key];
const required = requiredFields.includes(key)
? '<span class="api-param-required">必需</span>'
: '<span class="api-param-optional">可选</span>';
? '<span class="api-param-required">' + reqLabel + '</span>'
: '<span class="api-param-optional">' + optLabel + '</span>';
// 处理嵌套类型
let typeDisplay = prop.type || 'object';
@@ -338,16 +412,20 @@ function renderRequestBody(endpoint) {
}).join('');
if (rows) {
const pName = escapeHtml(_t('apiDocs.paramName'));
const tLabel = escapeHtml(_t('apiDocs.type'));
const dLabel = escapeHtml(_t('apiDocs.description'));
const exLabel = escapeHtml(_t('apiDocs.example'));
paramsTable = `
<div class="api-table-wrapper" style="margin-top: 12px;">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
<th>示例</th>
<th>${pName}</th>
<th>${tLabel}</th>
<th>${dLabel}</th>
<th>${reqLabel}</th>
<th>${exLabel}</th>
</tr>
</thead>
<tbody>
@@ -389,12 +467,12 @@ function renderRequestBody(endpoint) {
return `
<div class="api-section">
<div class="api-section-title">请求体</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionRequestBody'))}</div>
${endpoint.requestBody.description ? `<div class="api-description">${endpoint.requestBody.description}</div>` : ''}
${paramsTable}
${example ? `
<div style="margin-top: 16px;">
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">示例JSON:</div>
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(_t('apiDocs.exampleJson'))}</div>
<div class="api-response-example">
<pre>${escapeHtml(example)}</pre>
</div>
@@ -414,11 +492,11 @@ function renderResponses(endpoint) {
if (schema.example) {
example = JSON.stringify(schema.example, null, 2);
}
const descText = translateApiDocResponseDescFromResp(response);
return `
<div style="margin-bottom: 16px;">
<strong style="color: ${status.startsWith('2') ? 'var(--success-color)' : status.startsWith('4') ? 'var(--error-color)' : 'var(--warning-color)'}">${status}</strong>
${response.description ? `<span style="color: var(--text-secondary); margin-left: 8px;">${response.description}</span>` : ''}
${descText ? `<span style="color: var(--text-secondary); margin-left: 8px;">${escapeHtml(descText)}</span>` : ''}
${example ? `
<div class="api-response-example" style="margin-top: 8px;">
<pre>${escapeHtml(example)}</pre>
@@ -432,7 +510,7 @@ function renderResponses(endpoint) {
return `
<div class="api-section">
<div class="api-section-title">响应</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionResponse'))}</div>
${responseItems}
</div>
`;
@@ -462,8 +540,8 @@ function renderTestSection(endpoint) {
const bodyInputId = `test-body-${escapeId(path)}-${method}`;
bodyInput = `
<div class="api-test-input-group">
<label>请求体 (JSON)</label>
<textarea id="${bodyInputId}" class="test-body-input" placeholder='请输入JSON格式的请求体'>${defaultBody}</textarea>
<label>${escapeHtml(_t('apiDocs.requestBodyJson'))}</label>
<textarea id="${bodyInputId}" class="test-body-input" placeholder='${escapeHtml(_t('apiDocs.requestBodyPlaceholder'))}'>${defaultBody}</textarea>
</div>
`;
}
@@ -491,7 +569,7 @@ function renderTestSection(endpoint) {
const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`;
const defaultValue = param.schema?.default !== undefined ? param.schema.default : '';
const placeholder = param.description || param.name;
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">可选</span>';
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">' + escapeHtml(_t('apiDocs.optional')) + '</span>';
return `
<div class="api-test-input-group">
<label>${param.name} ${required}</label>
@@ -505,33 +583,40 @@ function renderTestSection(endpoint) {
}).join('');
}
const testSectionTitle = escapeHtml(_t('apiDocs.testSection'));
const queryParamsTitle = escapeHtml(_t('apiDocs.queryParams'));
const sendRequestLabel = escapeHtml(_t('apiDocs.sendRequest'));
const copyCurlLabel = escapeHtml(_t('apiDocs.copyCurl'));
const clearResultLabel = escapeHtml(_t('apiDocs.clearResult'));
const copyCurlTitle = escapeHtml(_t('apiDocs.copyCurlTitle'));
const clearResultTitle = escapeHtml(_t('apiDocs.clearResultTitle'));
return `
<div class="api-test-section">
<div class="api-section-title">测试接口</div>
<div class="api-section-title">${testSectionTitle}</div>
<div class="api-test-form">
${pathParamsInput}
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询参数:</div>${queryParamsInput}</div>` : ''}
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${queryParamsTitle}</div>${queryParamsInput}</div>` : ''}
${bodyInput}
<div class="api-test-buttons">
<button class="api-test-btn primary" onclick="testAPI('${method}', '${escapeHtml(path)}', '${endpoint.operationId || ''}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
发送请求
${sendRequestLabel}
</button>
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="复制curl命令">
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="${copyCurlTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
</svg>
复制curl
${copyCurlLabel}
</button>
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="清除测试结果">
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="${clearResultTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
清除结果
${clearResultLabel}
</button>
</div>
<div id="test-result-${escapeId(path)}-${method}" class="api-test-result" style="display: none;"></div>
@@ -548,7 +633,7 @@ async function testAPI(method, path, operationId) {
resultDiv.style.display = 'block';
resultDiv.className = 'api-test-result loading';
resultDiv.textContent = '发送请求中...';
resultDiv.textContent = _t('apiDocs.sendingRequest');
try {
// 替换路径参数
@@ -561,7 +646,7 @@ async function testAPI(method, path, operationId) {
if (input && input.value) {
actualPath = actualPath.replace(param, encodeURIComponent(input.value));
} else {
throw new Error(`路径参数 ${paramName} 不能为空`);
throw new Error(_t('apiDocs.errorPathParamRequired', { name: paramName }));
}
});
@@ -580,7 +665,7 @@ async function testAPI(method, path, operationId) {
if (input && input.value !== '' && input.value !== null && input.value !== undefined) {
queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`);
} else if (param.required) {
throw new Error(`查询参数 ${param.name} 不能为空`);
throw new Error(_t('apiDocs.errorQueryParamRequired', { name: param.name }));
}
});
}
@@ -602,8 +687,7 @@ async function testAPI(method, path, operationId) {
if (currentToken) {
options.headers['Authorization'] = 'Bearer ' + currentToken;
} else {
// 如果没有token,提示用户
throw new Error('未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token');
throw new Error(_t('apiDocs.errorTokenRequired'));
}
// 添加请求体
@@ -614,7 +698,7 @@ async function testAPI(method, path, operationId) {
try {
options.body = JSON.stringify(JSON.parse(bodyInput.value.trim()));
} catch (e) {
throw new Error('请求体JSON格式错误: ' + e.message);
throw new Error(_t('apiDocs.errorJsonInvalid') + e.message);
}
}
}
@@ -636,7 +720,7 @@ async function testAPI(method, path, operationId) {
} catch (error) {
resultDiv.className = 'api-test-result error';
resultDiv.textContent = '请求失败: ' + error.message;
resultDiv.textContent = _t('apiDocs.requestFailed') + error.message;
}
}
@@ -727,17 +811,17 @@ function copyCurlCommand(event, method, path) {
// 复制到剪贴板
const button = event ? event.target.closest('button') : null;
navigator.clipboard.writeText(curlCommand).then(() => {
// 显示成功提示
if (button) {
const originalText = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
button.style.color = 'var(--success-color)';
setTimeout(() => {
button.innerHTML = originalText;
button.style.color = '';
}, 2000);
} else {
alert('curl命令已复制到剪贴板!');
alert(_t('apiDocs.curlCopied'));
}
}).catch(err => {
console.error('复制失败:', err);
@@ -752,24 +836,25 @@ function copyCurlCommand(event, method, path) {
document.execCommand('copy');
if (button) {
const originalText = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
button.style.color = 'var(--success-color)';
setTimeout(() => {
button.innerHTML = originalText;
button.style.color = '';
}, 2000);
} else {
alert('curl命令已复制到剪贴板!');
alert(_t('apiDocs.curlCopied'));
}
} catch (e) {
alert('复制失败,请手动复制:\n\n' + curlCommand);
alert(_t('apiDocs.copyFailedManual') + curlCommand);
}
document.body.removeChild(textarea);
});
} catch (error) {
console.error('生成curl命令失败:', error);
alert('生成curl命令失败: ' + error.message);
alert(_t('apiDocs.curlGenFailed') + error.message);
}
}
@@ -935,10 +1020,10 @@ function toggleDescription(button) {
if (detail.style.display === 'none') {
detail.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
span.textContent = '隐藏详细说明';
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.hideDetailDesc') : '隐藏详细说明';
} else {
detail.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
span.textContent = '查看详细说明';
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.viewDetailDesc') : '查看详细说明';
}
}
+7 -7
View File
@@ -250,13 +250,13 @@ async function bootstrapApp() {
// 通用工具函数
function getStatusText(status) {
const statusMap = {
'pending': '等待中',
'running': '执行中',
'completed': '已完成',
'failed': '失败'
};
return statusMap[status] || status;
if (typeof window.t !== 'function') {
const fallback = { pending: '等待中', running: '执行中', completed: '已完成', failed: '失败' };
return fallback[status] || status;
}
const keyMap = { pending: 'mcpDetailModal.statusPending', running: 'mcpDetailModal.statusRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed' };
const key = keyMap[status];
return key ? window.t(key) : status;
}
function formatDuration(ms) {
+30 -31
View File
@@ -755,7 +755,7 @@ function renderMentionSuggestions({ showLoading = false } = {}) {
const disabledClass = toolEnabled ? '' : 'disabled';
const badge = tool.isExternal ? '<span class="mention-item-badge">外部</span>' : '<span class="mention-item-badge internal">内置</span>';
const nameHtml = escapeHtml(tool.name);
const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : '暂无描述';
const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : (typeof window.t === 'function' ? window.t('chat.noDescription') : '暂无描述');
const descHtml = `<div class="mention-item-desc">${description}</div>`;
// 根据工具在当前角色中的启用状态显示状态标签
const statusLabel = toolEnabled ? '可用' : (tool.roleEnabled !== undefined ? '已禁用(当前角色)' : '已禁用');
@@ -1209,7 +1209,7 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
mcpExecutionIds.forEach((execId, index) => {
const detailBtn = document.createElement('button');
detailBtn.className = 'mcp-detail-btn';
detailBtn.innerHTML = `<span>调用 #${index + 1}</span>`;
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
detailBtn.onclick = () => showMCPDetail(execId);
buttonsContainer.appendChild(detailBtn);
// 异步获取工具名称并更新按钮文本
@@ -1265,7 +1265,7 @@ function copyMessageToClipboard(messageDiv, button) {
showCopySuccess(button);
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动选择内容复制');
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
});
}
return;
@@ -1276,11 +1276,11 @@ function copyMessageToClipboard(messageDiv, button) {
showCopySuccess(button);
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动选择内容复制');
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
});
} catch (error) {
console.error('复制消息时出错:', error);
alert('复制失败,请手动选择内容复制');
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
}
}
@@ -1408,32 +1408,31 @@ function renderProcessDetails(messageId, processDetails) {
// 根据事件类型渲染不同的内容
let itemTitle = title;
if (eventType === 'iteration') {
itemTitle = `${data.iteration || 1} 轮迭代`;
itemTitle = (typeof window.t === 'function' ? window.t('chat.iterationRound', { n: data.iteration || 1 }) : '第 ' + (data.iteration || 1) + ' 轮迭代');
} else if (eventType === 'thinking') {
itemTitle = '🤔 AI思考';
itemTitle = '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考');
} else if (eventType === 'tool_calls_detected') {
itemTitle = `🔧 检测到 ${data.count || 0} 个工具调用`;
itemTitle = '🔧 ' + (typeof window.t === 'function' ? window.t('chat.toolCallsDetected', { count: data.count || 0 }) : '检测到 ' + (data.count || 0) + ' 个工具调用');
} else if (eventType === 'tool_call') {
const toolName = data.toolName || '未知工具';
const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const index = data.index || 0;
const total = data.total || 0;
itemTitle = `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`;
itemTitle = '🔧 ' + (typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')');
} else if (eventType === 'tool_result') {
const toolName = data.toolName || '未知工具';
const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const success = data.success !== false;
const statusIcon = success ? '✅' : '❌';
itemTitle = `${statusIcon} 工具 ${escapeHtml(toolName)} 执行${success ? '完成' : '失败'}`;
// 如果是知识检索工具,添加特殊标记
const execText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行失败');
itemTitle = statusIcon + ' ' + execText;
if (toolName === BuiltinTools.SEARCH_KNOWLEDGE_BASE && success) {
itemTitle = `📚 ${itemTitle} - 知识检索`;
itemTitle = '📚 ' + itemTitle + ' - ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrievalTag') : '知识检索');
}
} else if (eventType === 'knowledge_retrieval') {
itemTitle = '📚 知识检索';
itemTitle = '📚 ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrieval') : '知识检索');
} else if (eventType === 'error') {
itemTitle = '❌ 错误';
itemTitle = '❌ ' + (typeof window.t === 'function' ? window.t('chat.error') : '错误');
} else if (eventType === 'cancelled') {
itemTitle = '⛔ 任务已取消';
itemTitle = '⛔ ' + (typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消');
}
addTimelineItem(timeline, eventType, {
@@ -1509,7 +1508,7 @@ async function updateButtonWithToolName(button, executionId, index) {
const response = await apiFetch(`/api/monitor/execution/${executionId}`);
if (response.ok) {
const exec = await response.json();
const toolName = exec.toolName || '未知工具';
const toolName = exec.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
// 格式化工具名称(如果是 name::toolName 格式,只显示 toolName 部分)
const displayToolName = toolName.includes('::') ? toolName.split('::')[1] : toolName;
button.querySelector('span').textContent = `${displayToolName} #${index}`;
@@ -1528,7 +1527,7 @@ async function showMCPDetail(executionId) {
if (response.ok) {
// 填充模态框内容
document.getElementById('detail-tool-name').textContent = exec.toolName || 'Unknown';
document.getElementById('detail-tool-name').textContent = exec.toolName || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : 'Unknown');
document.getElementById('detail-execution-id').textContent = exec.id || 'N/A';
const statusEl = document.getElementById('detail-status');
const normalizedStatus = (exec.status || 'unknown').toLowerCase();
@@ -1598,22 +1597,22 @@ async function showMCPDetail(executionId) {
successText = content.text;
}
if (!successText) {
successText = '执行成功,未返回可展示的文本内容。';
successText = typeof window.t === 'function' ? window.t('mcpDetailModal.execSuccessNoContent') : '执行成功,未返回可展示的文本内容。';
}
successElement.textContent = successText;
}
}
} else {
responseElement.textContent = '暂无响应数据';
responseElement.textContent = typeof window.t === 'function' ? window.t('chat.noResponseData') : '暂无响应数据';
}
// 显示模态框
document.getElementById('mcp-detail-modal').style.display = 'block';
} else {
alert('获取详情失败: ' + (exec.error || '未知错误'));
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + (exec.error || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : '未知错误')));
}
} catch (error) {
alert('获取详情失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + error.message);
}
}
@@ -2255,7 +2254,7 @@ async function showAttackChain(conversationId) {
// 清空容器
const container = document.getElementById('attack-chain-container');
if (container) {
container.innerHTML = '<div class="loading-spinner">加载中...</div>';
container.innerHTML = '<div class="loading-spinner">' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '</div>';
}
// 隐藏详情面板
@@ -2355,7 +2354,7 @@ async function loadAttackChain(conversationId) {
console.error('加载攻击链失败:', error);
const container = document.getElementById('attack-chain-container');
if (container) {
container.innerHTML = `<div class="error-message">加载失败: ${error.message}</div>`;
container.innerHTML = '<div class="error-message">' + (typeof window.t === 'function' ? window.t('chat.loadFailed', { message: error.message }) : '加载失败: ' + error.message) + '</div>';
}
// 错误时也重置加载状态
setAttackChainLoading(conversationId, false);
@@ -2381,7 +2380,7 @@ function renderAttackChain(chainData) {
container.innerHTML = '';
if (!chainData.nodes || chainData.nodes.length === 0) {
container.innerHTML = '<div class="empty-message">暂无攻击链数据</div>';
container.innerHTML = '<div class="empty-message">' + (typeof window.t === 'function' ? window.t('chat.noAttackChainData') : '暂无攻击链数据') + '</div>';
return;
}
@@ -5544,9 +5543,9 @@ async function loadGroupConversations(groupId, searchQuery = '') {
// 显示加载状态
if (searchQuery) {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">搜索中...</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.searching') : '搜索中...') + '</div>';
} else {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '</div>';
}
// 构建URL,如果有搜索关键词则添加search参数
@@ -5558,7 +5557,7 @@ async function loadGroupConversations(groupId, searchQuery = '') {
const response = await apiFetch(url);
if (!response.ok) {
console.error(`Failed to load conversations for group ${groupId}:`, response.statusText);
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载失败,请重试</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.loadFailedRetry') : '加载失败,请重试') + '</div>';
return;
}
@@ -5572,7 +5571,7 @@ async function loadGroupConversations(groupId, searchQuery = '') {
// 验证返回的数据类型
if (!Array.isArray(groupConvs)) {
console.error(`Invalid response for group ${groupId}:`, groupConvs);
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">数据格式错误</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.dataFormatError') : '数据格式错误') + '</div>';
return;
}
+2 -2
View File
@@ -109,9 +109,9 @@
if (!label || typeof i18next === 'undefined') return;
const lang = (i18next.language || DEFAULT_LANG).toLowerCase();
if (lang.indexOf('zh') === 0) {
label.textContent = '中文';
label.textContent = i18next.t('lang.zhCN');
} else {
label.textContent = 'English';
label.textContent = i18next.t('lang.enUS');
}
}
+108 -80
View File
@@ -3,6 +3,27 @@ let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
// 将后端下发的进度文案转为当前语言的翻译(已知中文 key 映射)
function translateProgressMessage(message) {
if (!message || typeof message !== 'string') return message;
if (typeof window.t !== 'function') return message;
const trim = message.trim();
const map = {
'正在调用AI模型...': 'progress.callingAI',
'最后一次迭代:正在生成总结和下一步计划...': 'progress.lastIterSummary',
'总结生成完成': 'progress.summaryDone',
'正在生成最终回复...': 'progress.generatingFinalReply',
'达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary'
};
if (map[trim]) return window.t(map[trim]);
const callingToolPrefix = '正在调用工具: ';
if (trim.indexOf(callingToolPrefix) === 0) {
const name = trim.slice(callingToolPrefix.length);
return window.t('progress.callingTool', { name: name });
}
return message;
}
// 存储工具调用ID到DOM元素的映射,用于更新执行状态
const toolCallStatusMap = new Map();
@@ -57,11 +78,15 @@ function markProgressCancelling(progressId) {
}
}
function finalizeProgressTask(progressId, finalLabel = '已完成') {
function finalizeProgressTask(progressId, finalLabel) {
const stopBtn = document.getElementById(`${progressId}-stop-btn`);
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = finalLabel;
if (finalLabel !== undefined && finalLabel !== '') {
stopBtn.textContent = finalLabel;
} else {
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成';
}
}
progressTaskState.delete(progressId);
}
@@ -94,12 +119,15 @@ function addProgressMessage() {
const bubble = document.createElement('div');
bubble.className = 'message-bubble progress-container';
const progressTitleText = typeof window.t === 'function' ? window.t('chat.progressInProgress') : '渗透测试进行中...';
const stopTaskText = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务';
const collapseDetailText = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
bubble.innerHTML = `
<div class="progress-header">
<span class="progress-title">🔍 渗透测试进行中...</span>
<span class="progress-title">🔍 ${progressTitleText}</span>
<div class="progress-actions">
<button class="progress-stop" id="${id}-stop-btn" onclick="cancelProgressTask('${id}')">停止任务</button>
<button class="progress-toggle" onclick="toggleProgressDetails('${id}')">收起详情</button>
<button class="progress-stop" id="${id}-stop-btn" onclick="cancelProgressTask('${id}')">${stopTaskText}</button>
<button class="progress-toggle" onclick="toggleProgressDetails('${id}')">${collapseDetailText}</button>
</div>
</div>
<div class="progress-timeline expanded" id="${id}-timeline"></div>
@@ -123,10 +151,10 @@ function toggleProgressDetails(progressId) {
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
toggleBtn.textContent = '展开详情';
toggleBtn.textContent = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
} else {
timeline.classList.add('expanded');
toggleBtn.textContent = '收起详情';
toggleBtn.textContent = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
}
}
@@ -143,7 +171,7 @@ function collapseAllProgressDetails(assistantMessageId, progressId) {
timeline.classList.remove('expanded');
const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`);
if (btn) {
btn.innerHTML = '<span>展开详情</span>';
btn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
}
}
}
@@ -246,7 +274,7 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
// 设置详情内容(如果有错误,默认折叠;否则默认折叠)
detailsContainer.innerHTML = `
<div class="process-details-content">
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情</div>'}
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>'}
</div>
`;
@@ -258,10 +286,9 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
timeline.classList.remove('expanded');
}
// 更新按钮文本为"展开详情"(因为默认折叠)
const processDetailBtn = buttonsContainer.querySelector('.process-detail-btn');
if (processDetailBtn) {
processDetailBtn.innerHTML = '<span>展开详情</span>';
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
}
}
@@ -279,22 +306,23 @@ function toggleProcessDetails(progressId, assistantMessageId) {
const timeline = detailsContainer.querySelector('.progress-timeline');
const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`);
const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
if (content && timeline) {
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
if (btn) btn.innerHTML = '<span>展开详情</span>';
if (btn) btn.innerHTML = '<span>' + expandT + '</span>';
} else {
timeline.classList.add('expanded');
if (btn) btn.innerHTML = '<span>收起详情</span>';
if (btn) btn.innerHTML = '<span>' + collapseT + '</span>';
}
} else if (timeline) {
// 如果只有timeline,直接切换
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
if (btn) btn.innerHTML = '<span>展开详情</span>';
if (btn) btn.innerHTML = '<span>' + expandT + '</span>';
} else {
timeline.classList.add('expanded');
if (btn) btn.innerHTML = '<span>收起详情</span>';
if (btn) btn.innerHTML = '<span>' + collapseT + '</span>';
}
}
@@ -338,10 +366,10 @@ async function cancelProgressTask(progressId) {
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert('取消任务失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message);
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.textContent = '停止任务';
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务';
}
const currentState = progressTaskState.get(progressId);
if (currentState) {
@@ -391,15 +419,17 @@ function convertProgressToDetails(progressId, assistantMessageId) {
// 如果有错误,默认折叠;否则默认展开
const shouldExpand = !hasError;
const expandedClass = shouldExpand ? 'expanded' : '';
const toggleText = shouldExpand ? '收起详情' : '展开详情';
// 总是显示详情组件,即使没有内容也显示
const collapseDetailText = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
const expandDetailText = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
const toggleText = shouldExpand ? collapseDetailText : expandDetailText;
const penetrationDetailText = typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情';
const noProcessDetailText = typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)';
bubble.innerHTML = `
<div class="progress-header">
<span class="progress-title">📋 渗透测试详情</span>
<span class="progress-title">📋 ${penetrationDetailText}</span>
${hasContent ? `<button class="progress-toggle" onclick="toggleProgressDetails('${detailsId}')">${toggleText}</button>` : ''}
</div>
${hasContent ? `<div class="progress-timeline ${expandedClass}" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情(可能执行过快或未触发详细事件)</div>'}
${hasContent ? `<div class="progress-timeline ${expandedClass}" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">' + noProcessDetailText + '</div>'}
`;
contentWrapper.appendChild(bubble);
@@ -466,41 +496,37 @@ function handleStreamEvent(event, progressElement, progressId,
case 'iteration':
// 添加迭代标记
addTimelineItem(timeline, 'iteration', {
title: `${event.data?.iteration || 1} 轮迭代`,
title: typeof window.t === 'function' ? window.t('chat.iterationRound', { n: event.data?.iteration || 1 }) : '第 ' + (event.data?.iteration || 1) + ' 轮迭代',
message: event.message,
data: event.data
});
break;
case 'thinking':
// 显示AI思考内容
addTimelineItem(timeline, 'thinking', {
title: '🤔 AI思考',
title: '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考'),
message: event.message,
data: event.data
});
break;
case 'tool_calls_detected':
// 工具调用检测
addTimelineItem(timeline, 'tool_calls_detected', {
title: `🔧 检测到 ${event.data?.count || 0} 个工具调用`,
title: '🔧 ' + (typeof window.t === 'function' ? window.t('chat.toolCallsDetected', { count: event.data?.count || 0 }) : '检测到 ' + (event.data?.count || 0) + ' 个工具调用'),
message: event.message,
data: event.data
});
break;
case 'tool_call':
// 显示工具调用信息
const toolInfo = event.data || {};
const toolName = toolInfo.toolName || '未知工具';
const toolName = toolInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const index = toolInfo.index || 0;
const total = toolInfo.total || 0;
const toolCallId = toolInfo.toolCallId || null;
// 添加工具调用项,并标记为执行中
const toolCallTitle = typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')';
const toolCallItemId = addTimelineItem(timeline, 'tool_call', {
title: `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`,
title: '🔧 ' + toolCallTitle,
message: event.message,
data: toolInfo,
expanded: false
@@ -519,22 +545,18 @@ function handleStreamEvent(event, progressElement, progressId,
break;
case 'tool_result':
// 显示工具执行结果
const resultInfo = event.data || {};
const resultToolName = resultInfo.toolName || '未知工具';
const resultToolName = resultInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const success = resultInfo.success !== false;
const statusIcon = success ? '✅' : '❌';
const resultToolCallId = resultInfo.toolCallId || null;
// 如果有关联的toolCallId,更新工具调用项的状态
const resultExecText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行失败');
if (resultToolCallId && toolCallStatusMap.has(resultToolCallId)) {
updateToolCallStatus(resultToolCallId, success ? 'completed' : 'failed');
// 从映射中移除(已完成)
toolCallStatusMap.delete(resultToolCallId);
}
addTimelineItem(timeline, 'tool_result', {
title: `${statusIcon} 工具 ${escapeHtml(resultToolName)} 执行${success ? '完成' : '失败'}`,
title: statusIcon + ' ' + resultExecText,
message: event.message,
data: resultInfo,
expanded: false
@@ -542,36 +564,30 @@ function handleStreamEvent(event, progressElement, progressId,
break;
case 'progress':
// 更新进度状态
const progressTitle = document.querySelector(`#${progressId} .progress-title`);
if (progressTitle) {
progressTitle.textContent = '🔍 ' + event.message;
const progressMsg = translateProgressMessage(event.message);
progressTitle.textContent = '🔍 ' + progressMsg;
}
break;
case 'cancelled':
// 显示错误
const taskCancelledText = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消';
addTimelineItem(timeline, 'cancelled', {
title: '⛔ 任务已取消',
title: '⛔ ' + taskCancelledText,
message: event.message,
data: event.data
});
// 更新进度标题为取消状态
const cancelTitle = document.querySelector(`#${progressId} .progress-title`);
if (cancelTitle) {
cancelTitle.textContent = '⛔ 任务已取消';
cancelTitle.textContent = '⛔ ' + taskCancelledText;
}
// 更新进度容器为已完成状态(添加completed类)
const cancelProgressContainer = document.querySelector(`#${progressId} .progress-container`);
if (cancelProgressContainer) {
cancelProgressContainer.classList.add('completed');
}
// 完成进度任务(标记为已取消)
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已取消');
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCancelled') : '已取消');
}
// 如果取消事件包含messageId,说明有助手消息,需要显示取消内容
@@ -689,7 +705,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 完成进度任务(标记为失败)
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已失败');
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusFailed') : '执行失败');
}
// 如果错误事件包含messageId,说明有助手消息,需要显示错误内容
@@ -753,7 +769,7 @@ function handleStreamEvent(event, progressElement, progressId,
updateProgressConversation(progressId, event.data.conversationId);
}
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已完成');
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成');
}
// 检查时间线中是否有错误项
@@ -807,17 +823,19 @@ function updateToolCallStatus(toolCallId, status) {
// 移除之前的状态类
item.classList.remove('tool-call-running', 'tool-call-completed', 'tool-call-failed');
// 根据状态更新样式和文本
const runningLabel = typeof window.t === 'function' ? window.t('timeline.running') : '执行中...';
const completedLabel = typeof window.t === 'function' ? window.t('timeline.completed') : '已完成';
const failedLabel = typeof window.t === 'function' ? window.t('timeline.execFailed') : '执行失败';
let statusText = '';
if (status === 'running') {
item.classList.add('tool-call-running');
statusText = ' <span class="tool-status-badge tool-status-running">执行中...</span>';
statusText = ' <span class="tool-status-badge tool-status-running">' + escapeHtml(runningLabel) + '</span>';
} else if (status === 'completed') {
item.classList.add('tool-call-completed');
statusText = ' <span class="tool-status-badge tool-status-completed">✅ 已完成</span>';
statusText = ' <span class="tool-status-badge tool-status-completed">✅ ' + escapeHtml(completedLabel) + '</span>';
} else if (status === 'failed') {
item.classList.add('tool-call-failed');
statusText = ' <span class="tool-status-badge tool-status-failed">❌ 执行失败</span>';
statusText = ' <span class="tool-status-badge tool-status-failed">❌ ' + escapeHtml(failedLabel) + '</span>';
}
// 更新标题(保留原有文本,追加状态)
@@ -869,11 +887,12 @@ function addTimelineItem(timeline, type, options) {
} else if (type === 'tool_call' && options.data) {
const data = options.data;
const args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : {});
const paramsLabel = typeof window.t === 'function' ? window.t('timeline.params') : '参数:';
content += `
<div class="timeline-item-content">
<div class="tool-details">
<div class="tool-arg-section">
<strong>参数:</strong>
<strong>${escapeHtml(paramsLabel)}</strong>
<pre class="tool-args">${escapeHtml(JSON.stringify(args, null, 2))}</pre>
</div>
</div>
@@ -882,22 +901,25 @@ function addTimelineItem(timeline, type, options) {
} else if (type === 'tool_result' && options.data) {
const data = options.data;
const isError = data.isError || !data.success;
const result = data.result || data.error || '无结果';
// 确保 result 是字符串
const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果';
const result = data.result || data.error || noResultText;
const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
const execResultLabel = typeof window.t === 'function' ? window.t('timeline.executionResult') : '执行结果:';
const execIdLabel = typeof window.t === 'function' ? window.t('timeline.executionId') : '执行ID:';
content += `
<div class="timeline-item-content">
<div class="tool-result-section ${isError ? 'error' : 'success'}">
<strong>执行结果:</strong>
<strong>${escapeHtml(execResultLabel)}</strong>
<pre class="tool-result">${escapeHtml(resultStr)}</pre>
${data.executionId ? `<div class="tool-execution-id">执行ID: <code>${escapeHtml(data.executionId)}</code></div>` : ''}
${data.executionId ? `<div class="tool-execution-id">${escapeHtml(execIdLabel)} <code>${escapeHtml(data.executionId)}</code></div>` : ''}
</div>
</div>
`;
} else if (type === 'cancelled') {
const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消';
content += `
<div class="timeline-item-content">
${escapeHtml(options.message || '任务已取消')}
${escapeHtml(options.message || taskCancelledLabel)}
</div>
`;
}
@@ -964,26 +986,28 @@ function renderActiveTasks(tasks) {
? startedTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
: '';
// 根据任务状态显示不同的文本
const _t = function (k) { return typeof window.t === 'function' ? window.t(k) : k; };
const statusMap = {
'running': '执行中',
'cancelling': '取消中',
'failed': '执行失败',
'timeout': '执行超时',
'cancelled': '已取消',
'completed': '已完成'
'running': _t('tasks.statusRunning'),
'cancelling': _t('tasks.statusCancelling'),
'failed': _t('tasks.statusFailed'),
'timeout': _t('tasks.statusTimeout'),
'cancelled': _t('tasks.statusCancelled'),
'completed': _t('tasks.statusCompleted')
};
const statusText = statusMap[task.status] || '执行中';
const statusText = statusMap[task.status] || _t('tasks.statusRunning');
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed'].includes(task.status);
const unnamedTaskText = _t('tasks.unnamedTask');
const stopTaskBtnText = _t('tasks.stopTask');
item.innerHTML = `
<div class="active-task-info">
<span class="active-task-status">${statusText}</span>
<span class="active-task-message">${escapeHtml(task.message || '未命名任务')}</span>
<span class="active-task-message">${escapeHtml(task.message || unnamedTaskText)}</span>
</div>
<div class="active-task-actions">
${timeText ? `<span class="active-task-time">${timeText}</span>` : ''}
${!isFinalStatus ? '<button class="active-task-cancel">停止任务</button>' : ''}
${!isFinalStatus ? '<button class="active-task-cancel">' + stopTaskBtnText + '</button>' : ''}
</div>
`;
@@ -994,7 +1018,7 @@ function renderActiveTasks(tasks) {
cancelBtn.onclick = () => cancelActiveTask(task.conversationId, cancelBtn);
if (task.status === 'cancelling') {
cancelBtn.disabled = true;
cancelBtn.textContent = '取消中...';
cancelBtn.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...';
}
}
}
@@ -1007,14 +1031,14 @@ async function cancelActiveTask(conversationId, button) {
if (!conversationId) return;
const originalText = button.textContent;
button.disabled = true;
button.textContent = '取消中...';
button.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...';
try {
await requestCancel(conversationId);
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert('取消任务失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message);
button.disabled = false;
button.textContent = originalText;
}
@@ -1522,14 +1546,14 @@ function updateBatchActionsState() {
if (batchActions) {
batchActions.style.display = 'flex';
}
if (selectedCountSpan) {
selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : `已选择 ${selectedCount}`;
}
} else {
if (batchActions) {
batchActions.style.display = 'none';
}
}
if (selectedCountSpan) {
selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : '已选择 ' + selectedCount + ' 项';
}
// 更新全选复选框状态
const selectAllCheckbox = document.getElementById('monitor-select-all');
@@ -1655,3 +1679,7 @@ function formatExecutionDuration(start, end) {
}
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHoursOnly', { hours: hours }) : hours + ' 小时';
}
document.addEventListener('languagechange', function () {
updateBatchActionsState();
});
+5
View File
@@ -1433,6 +1433,11 @@ document.addEventListener('DOMContentLoaded', () => {
updateRoleSelectorDisplay();
});
// 语言切换后刷新角色选择器显示(默认/自定义角色名)
document.addEventListener('languagechange', () => {
updateRoleSelectorDisplay();
});
// 获取当前选中的角色(供chat.js使用)
function getCurrentRole() {
return currentRole || '';