Add files via upload

This commit is contained in:
公明
2026-07-09 15:55:55 +08:00
committed by GitHub
parent 56ee11a97a
commit 1c576db75d
7 changed files with 1188 additions and 114 deletions
+299 -13
View File
@@ -2463,6 +2463,7 @@ function syncProcessDetailButtonLabels(messageId, expanded) {
function renderProcessDetails(messageId, processDetails, options) {
const renderOpts = options || {};
const appendMode = !!renderOpts.append;
const prependMode = !!renderOpts.prepend;
const markLoaded = renderOpts.markLoaded !== false;
const messageElement = document.getElementById(messageId);
if (!messageElement) {
@@ -2546,13 +2547,21 @@ function renderProcessDetails(messageId, processDetails, options) {
processDetails = mergeMessageReasoningContentIntoProcessDetails(processDetails, reasoningFromMessage);
processDetails = filterNoiseProcessDetails(processDetails);
processDetails = dedupeConsecutiveProcessDetailRows(processDetails);
const renderedMcpIds = collectMcpExecutionIdsFromProcessDetails(processDetails);
if (renderedMcpIds.length > 0) {
setPendingMcpExecutionIds(messageElement, renderedMcpIds);
}
const renderedToolCount = processDetails.filter((d) => d && d.eventType === 'tool_call').length;
if (renderedToolCount > 0) {
setMcpExecutionSummaryCount(messageElement, renderedToolCount);
}
if (typeof window.coalesceProcessDetailsToolPairs === 'function') {
processDetails = window.coalesceProcessDetailsToolPairs(processDetails);
}
processDetails = compactWorkflowProcessDetails(processDetails);
// 如果没有processDetails或为空,显示空状态
if (!processDetails || processDetails.length === 0) {
if (!appendMode) {
if (!appendMode && !prependMode) {
timeline.innerHTML = '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>';
if (!isProcessDetailsUserExpanded(messageId)) {
timeline.classList.remove('expanded');
@@ -2561,7 +2570,13 @@ function renderProcessDetails(messageId, processDetails, options) {
return;
}
if (!appendMode) {
const prependAnchor = prependMode ? timeline.firstChild : null;
const prependScrollBox = prependMode ? document.getElementById('chat-messages') : null;
const prependScrollHeight = prependScrollBox ? prependScrollBox.scrollHeight : 0;
const prependScrollTop = prependScrollBox ? prependScrollBox.scrollTop : 0;
const prependedIds = [];
if (!appendMode && !prependMode) {
timeline.innerHTML = '';
}
@@ -2713,7 +2728,24 @@ function renderProcessDetails(messageId, processDetails, options) {
if (eventType === 'tool_call' && data._mergedResult) {
timelineOpts.mergedResult = data._mergedResult;
}
addTimelineItem(timeline, eventType, timelineOpts);
const itemId = addTimelineItem(timeline, eventType, timelineOpts);
if (prependMode && itemId) {
prependedIds.push(itemId);
}
}
function finishPrependRender() {
if (!prependMode || prependedIds.length === 0) return;
const fragment = document.createDocumentFragment();
prependedIds.forEach((id) => {
const node = document.getElementById(id);
if (node) fragment.appendChild(node);
});
timeline.insertBefore(fragment, prependAnchor || timeline.firstChild);
if (prependScrollBox) {
const delta = prependScrollBox.scrollHeight - prependScrollHeight;
prependScrollBox.scrollTop = prependScrollTop + delta;
}
}
const TIMELINE_RENDER_BATCH = 40;
@@ -2725,13 +2757,17 @@ function renderProcessDetails(messageId, processDetails, options) {
if (endIdx < processDetails.length) {
requestAnimationFrame(() => renderTimelineBatch(endIdx));
} else if (markLoaded) {
finishPrependRender();
finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoaded, timeline);
} else {
finishPrependRender();
}
};
if (processDetails.length > TIMELINE_RENDER_BATCH) {
renderTimelineBatch(0);
} else {
processDetails.forEach(renderOneProcessDetail);
finishPrependRender();
if (markLoaded) {
finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoaded, timeline);
}
@@ -2781,6 +2817,18 @@ function prefetchProcessDetailsSummaryHint(messageId, messageElement) {
const j = await res.json().catch(() => ({}));
if (!res.ok || !j.summary) return;
const s = j.summary;
const toolCount = parseInt(s.toolCount, 10) || 0;
const summaryMcpIds = Array.isArray(s.mcpExecutionIds) ? s.mcpExecutionIds : [];
const summaryTools = Array.isArray(s.toolExecutions) ? s.toolExecutions : [];
if (summaryTools.length > 0) {
setPendingToolExecutionSummaries(messageElement, summaryTools);
}
if (summaryMcpIds.length > 0) {
setPendingMcpExecutionIds(messageElement, summaryMcpIds);
}
if (toolCount > 0) {
setMcpExecutionSummaryCount(messageElement, toolCount);
}
const timeline = detailsContainer.querySelector('.progress-timeline');
if (!timeline || detailsContainer.dataset.loaded === '1') return;
const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
@@ -2871,16 +2919,114 @@ function getPendingMcpExecutionCount(messageElement) {
}
}
function getPendingToolExecutionSummaryCount(messageElement) {
if (!messageElement || !messageElement.dataset || !messageElement.dataset.pendingToolExecutionSummaries) {
return 0;
}
try {
const tools = JSON.parse(messageElement.dataset.pendingToolExecutionSummaries);
return Array.isArray(tools) ? tools.length : 0;
} catch (e) {
return 0;
}
}
function getMcpExecutionCount(messageElement) {
const pending = getPendingMcpExecutionCount(messageElement);
if (pending > 0) return pending;
const pendingSummaries = getPendingToolExecutionSummaryCount(messageElement);
if (pendingSummaries > 0) return pendingSummaries;
const toolList = messageElement && messageElement.querySelector('.mcp-tool-list');
if (toolList) {
return toolList.querySelectorAll('.mcp-detail-btn[data-exec-id]').length;
const rendered = toolList.querySelectorAll('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]').length;
if (rendered > 0) return rendered;
}
if (messageElement && messageElement.dataset && messageElement.dataset.mcpExecutionCount) {
const summaryCount = parseInt(messageElement.dataset.mcpExecutionCount, 10) || 0;
if (summaryCount > 0) return summaryCount;
}
return 0;
}
function collectMcpExecutionIdsFromProcessDetails(processDetails) {
if (!Array.isArray(processDetails)) return [];
const seen = new Set();
const ids = [];
const add = (value) => {
const id = value == null ? '' : String(value).trim();
if (!id || seen.has(id)) return;
seen.add(id);
ids.push(id);
};
processDetails.forEach((detail) => {
const data = detail && detail.data && typeof detail.data === 'object' ? detail.data : null;
if (!data) return;
add(data.executionId);
const merged = data._mergedResult && typeof data._mergedResult === 'object' ? data._mergedResult : null;
if (merged) add(merged.executionId);
});
return ids;
}
function setPendingMcpExecutionIds(messageElement, executionIds) {
if (!messageElement || !messageElement.dataset || !Array.isArray(executionIds)) return;
const seen = new Set();
const ids = [];
executionIds.forEach((value) => {
const id = value == null ? '' : String(value).trim();
if (!id || seen.has(id)) return;
seen.add(id);
ids.push(id);
});
if (ids.length > 0) {
messageElement.dataset.pendingMcpExecutionIds = JSON.stringify(ids);
} else {
delete messageElement.dataset.pendingMcpExecutionIds;
}
if (typeof syncMcpToolsToggleButton === 'function') {
syncMcpToolsToggleButton(messageElement);
}
}
function normalizeToolExecutionSummaryForButton(raw) {
const data = raw && typeof raw === 'object' ? raw : {};
return {
toolName: data.toolName || data.name || '',
status: data.status || '',
executionId: data.executionId || '',
toolCallId: data.toolCallId || '',
processDetailId: data.processDetailId || ''
};
}
function setPendingToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
const normalized = summaries
.map(normalizeToolExecutionSummaryForButton)
.filter((item) => item.toolName || item.executionId || item.toolCallId);
if (normalized.length > 0) {
messageElement.dataset.pendingToolExecutionSummaries = JSON.stringify(normalized);
} else {
delete messageElement.dataset.pendingToolExecutionSummaries;
}
if (typeof syncMcpToolsToggleButton === 'function') {
syncMcpToolsToggleButton(messageElement);
}
}
function setMcpExecutionSummaryCount(messageElement, count) {
if (!messageElement || !messageElement.dataset) return;
const n = parseInt(count, 10) || 0;
if (n > 0) {
messageElement.dataset.mcpExecutionCount = String(n);
} else {
delete messageElement.dataset.mcpExecutionCount;
}
if (typeof syncMcpToolsToggleButton === 'function') {
syncMcpToolsToggleButton(messageElement);
}
}
function formatMcpToolsToggleLabel(count, expanded) {
if (expanded) {
if (typeof window.t === 'function') {
@@ -2986,12 +3132,92 @@ function syncMcpToolsToggleButton(messageElement) {
toolsToggle.innerHTML = '<span>' + formatMcpToolsToggleLabel(count, expanded) + '</span>';
}
function cssEscapeValue(value) {
const s = String(value || '');
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
return CSS.escape(s);
}
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
async function ensureProcessDetailsForToolFocus(messageElement, anchorId) {
if (!messageElement || !messageElement.id) return null;
const detailsId = 'process-details-' + messageElement.id;
let detailsContainer = document.getElementById(detailsId);
const backendId = messageElement.dataset ? String(messageElement.dataset.backendMessageId || '').trim() : '';
if (detailsContainer && detailsContainer.dataset && detailsContainer.dataset.lazyNotLoaded === '1' && backendId && typeof window.loadProcessDetailsPaginated === 'function') {
await window.loadProcessDetailsPaginated(messageElement.id, backendId, { autoLoadAll: false, anchorId: anchorId || '' });
detailsContainer = document.getElementById(detailsId);
} else if (!detailsContainer && backendId && typeof renderProcessDetails === 'function') {
renderProcessDetails(messageElement.id, null);
detailsContainer = document.getElementById(detailsId);
if (detailsContainer && typeof window.loadProcessDetailsPaginated === 'function') {
await window.loadProcessDetailsPaginated(messageElement.id, backendId, { autoLoadAll: false, anchorId: anchorId || '' });
detailsContainer = document.getElementById(detailsId);
}
}
if (typeof window.expandProcessDetailsTimeline === 'function') {
window.expandProcessDetailsTimeline(messageElement.id);
} else if (typeof toggleProcessDetails === 'function') {
const timeline = detailsContainer && detailsContainer.querySelector('.progress-timeline');
if (!timeline || !timeline.classList.contains('expanded')) {
toggleProcessDetails(null, messageElement.id);
}
}
return document.getElementById(detailsId);
}
async function focusToolExecutionInProcessDetails(messageElement, summary, index) {
const item = normalizeToolExecutionSummaryForButton(summary);
let detailsContainer = await ensureProcessDetailsForToolFocus(messageElement, item.processDetailId || '');
let timeline = detailsContainer && detailsContainer.querySelector('.progress-timeline');
if (!timeline) return;
let target = null;
if (item.processDetailId) {
target = timeline.querySelector('[data-process-detail-id="' + cssEscapeValue(item.processDetailId) + '"]');
}
if (!target && item.toolCallId) {
target = timeline.querySelector('[data-tool-call-id="' + cssEscapeValue(item.toolCallId) + '"]');
}
if (!target) {
const toolItems = timeline.querySelectorAll('.timeline-item-tool_call');
target = toolItems[index] || null;
}
if (!target && item.processDetailId && messageElement.dataset && messageElement.dataset.backendMessageId && typeof window.loadProcessDetailsPaginated === 'function') {
await window.loadProcessDetailsPaginated(messageElement.id, messageElement.dataset.backendMessageId, {
autoLoadAll: false,
anchorId: item.processDetailId
});
detailsContainer = document.getElementById('process-details-' + messageElement.id);
timeline = detailsContainer && detailsContainer.querySelector('.progress-timeline');
target = timeline ? timeline.querySelector('[data-process-detail-id="' + cssEscapeValue(item.processDetailId) + '"]') : null;
}
if (!target) return;
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
target.classList.remove('timeline-item-focus-pulse');
void target.offsetWidth;
target.classList.add('timeline-item-focus-pulse');
setTimeout(() => {
target.classList.remove('timeline-item-focus-pulse');
}, 2200);
}
function toggleMcpToolList(assistantMessageId) {
const messageEl = document.getElementById(assistantMessageId);
if (!messageEl) return;
const chrome = ensureMcpCallSectionChrome(messageEl, assistantMessageId);
if (!chrome) return;
const { toolList } = chrome;
if (
!getPendingMcpExecutionCount(messageEl) &&
!getPendingToolExecutionSummaryCount(messageEl) &&
!toolList.querySelector('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]')
) {
if (typeof toggleProcessDetails === 'function') {
toggleProcessDetails(null, assistantMessageId);
}
return;
}
const willExpand = !toolList.classList.contains('expanded');
if (willExpand) {
ensureMcpCallButtons(messageEl);
@@ -3007,6 +3233,9 @@ window.syncMcpToolsToggleButton = syncMcpToolsToggleButton;
window.isProcessDetailsUserExpanded = isProcessDetailsUserExpanded;
window.syncProcessDetailButtonLabels = syncProcessDetailButtonLabels;
window.ensureMcpCallSectionChrome = ensureMcpCallSectionChrome;
window.setMcpExecutionSummaryCount = setMcpExecutionSummaryCount;
window.setPendingMcpExecutionIds = setPendingMcpExecutionIds;
window.setPendingToolExecutionSummaries = setPendingToolExecutionSummaries;
/** 将 MCP 工具按钮挂到独立工具列表,并批量解析工具名 */
function appendMcpCallButtons(messageElement, executionIds) {
@@ -3033,28 +3262,85 @@ function appendMcpCallButtons(messageElement, executionIds) {
syncMcpToolsToggleButton(messageElement);
}
function appendMcpCallSummaryButtons(messageElement, summaries) {
if (!messageElement || !Array.isArray(summaries) || summaries.length === 0) {
return;
}
const chrome = ensureMcpCallSectionChrome(messageElement, messageElement.id);
if (!chrome) return;
const toolList = chrome.toolList;
summaries.forEach((raw, index) => {
const item = normalizeToolExecutionSummaryForButton(raw);
const key = item.executionId || item.toolCallId || `${item.toolName || 'tool'}-${index + 1}`;
const selector = '.mcp-detail-btn[data-tool-summary="' + CSS.escape(String(key)) + '"]';
if (toolList.querySelector(selector) || (item.executionId && toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(item.executionId)) + '"]'))) {
return;
}
const btn = document.createElement('button');
btn.className = 'mcp-detail-btn';
btn.dataset.toolSummary = key;
if (item.executionId) {
btn.dataset.execId = item.executionId;
}
if (item.processDetailId || item.toolCallId) {
btn.onclick = async () => {
await focusToolExecutionInProcessDetails(messageElement, item, index);
};
} else if (item.executionId) {
btn.onclick = () => showMCPDetail(item.executionId);
} else {
btn.onclick = async () => {
await focusToolExecutionInProcessDetails(messageElement, item, index);
};
}
renderToolExecutionButtonContent(btn, item.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'), String(index + 1), item.status);
toolList.appendChild(btn);
});
syncMcpToolsToggleButton(messageElement);
}
/** 历史会话懒加载:用户展开工具列表时再渲染工具按钮 */
function ensureMcpCallButtons(messageElement) {
if (!messageElement || !messageElement.dataset || !messageElement.dataset.pendingMcpExecutionIds) {
if (!messageElement || !messageElement.dataset) {
return;
}
let executionIds;
try {
executionIds = JSON.parse(messageElement.dataset.pendingMcpExecutionIds);
} catch (e) {
let renderedSummaryExecutions = false;
if (messageElement.dataset.pendingToolExecutionSummaries) {
let summaries;
try {
summaries = JSON.parse(messageElement.dataset.pendingToolExecutionSummaries);
} catch (e) {
delete messageElement.dataset.pendingToolExecutionSummaries;
summaries = [];
}
if (Array.isArray(summaries) && summaries.length > 0) {
appendMcpCallSummaryButtons(messageElement, summaries);
renderedSummaryExecutions = true;
}
delete messageElement.dataset.pendingToolExecutionSummaries;
}
if (renderedSummaryExecutions) {
delete messageElement.dataset.pendingMcpExecutionIds;
return;
}
if (!Array.isArray(executionIds) || executionIds.length === 0) {
if (messageElement.dataset.pendingMcpExecutionIds) {
let executionIds;
try {
executionIds = JSON.parse(messageElement.dataset.pendingMcpExecutionIds);
} catch (e) {
delete messageElement.dataset.pendingMcpExecutionIds;
executionIds = [];
}
if (Array.isArray(executionIds) && executionIds.length > 0) {
appendMcpCallButtons(messageElement, executionIds);
}
delete messageElement.dataset.pendingMcpExecutionIds;
return;
}
appendMcpCallButtons(messageElement, executionIds);
delete messageElement.dataset.pendingMcpExecutionIds;
}
window.ensureMcpCallButtons = ensureMcpCallButtons;
window.appendMcpCallButtons = appendMcpCallButtons;
window.appendMcpCallSummaryButtons = appendMcpCallSummaryButtons;
function normalizeToolExecutionSummary(raw) {
if (typeof raw === 'string') {
+88 -19
View File
@@ -1376,27 +1376,71 @@ function getProcessDetailsLoadMoreLabel(hasMore) {
}
function updateProcessDetailsLoadMoreButton(assistantMessageId, backendMessageId, hasMore) {
updateProcessDetailsPaginationButtons(assistantMessageId, backendMessageId, {
hasPrev: false,
hasNext: hasMore
});
}
function updateProcessDetailsPaginationButtons(assistantMessageId, backendMessageId, pageState) {
const detailsContainer = document.getElementById('process-details-' + assistantMessageId);
if (!detailsContainer) return;
const state = pageState || {};
let btn = detailsContainer.querySelector('.process-details-load-more-btn');
if (!hasMore) {
if (btn) btn.remove();
let prevBtn = detailsContainer.querySelector('.process-details-load-prev-btn');
if (!state.hasPrev) {
if (prevBtn) prevBtn.remove();
} else {
if (!prevBtn) {
prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.className = 'mcp-detail-btn process-details-load-prev-btn';
const content = detailsContainer.querySelector('.process-details-content');
if (content) {
detailsContainer.insertBefore(prevBtn, content);
} else {
detailsContainer.prepend(prevBtn);
}
}
const loadMoreText = typeof window.t === 'function' ? window.t('common.loadMore') : '加载更多';
let prevPageText = typeof window.t === 'function' ? window.t('chat.previousPage') : '上一页';
if (!prevPageText || prevPageText === 'chat.previousPage') prevPageText = '上一页';
prevBtn.textContent = loadMoreText + ' · ' + prevPageText;
prevBtn.disabled = false;
prevBtn.onclick = async () => {
if (detailsContainer.dataset.loadingPrev === '1') return;
detailsContainer.dataset.loadingPrev = '1';
prevBtn.disabled = true;
prevBtn.textContent = typeof window.t === 'function' ? window.t('common.loading') : '加载中…';
try {
await loadProcessDetailsPaginated(assistantMessageId, backendMessageId, {
prepend: true,
autoLoadAll: false
});
} finally {
detailsContainer.dataset.loadingPrev = '0';
}
};
}
let nextBtn = detailsContainer.querySelector('.process-details-load-more-btn');
if (!state.hasNext) {
if (nextBtn) nextBtn.remove();
return;
}
if (!btn) {
btn = document.createElement('button');
btn.type = 'button';
btn.className = 'mcp-detail-btn process-details-load-more-btn';
detailsContainer.appendChild(btn);
if (!nextBtn) {
nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.className = 'mcp-detail-btn process-details-load-more-btn';
detailsContainer.appendChild(nextBtn);
}
btn.textContent = getProcessDetailsLoadMoreLabel(true);
btn.disabled = false;
btn.onclick = async () => {
nextBtn.textContent = getProcessDetailsLoadMoreLabel(true);
nextBtn.disabled = false;
nextBtn.onclick = async () => {
if (detailsContainer.dataset.loadingMore === '1') return;
detailsContainer.dataset.loadingMore = '1';
btn.disabled = true;
btn.textContent = typeof window.t === 'function' ? window.t('common.loading') : '加载中…';
nextBtn.disabled = true;
nextBtn.textContent = typeof window.t === 'function' ? window.t('common.loading') : '加载中…';
try {
await loadProcessDetailsPaginated(assistantMessageId, backendMessageId, {
append: true,
@@ -1420,14 +1464,28 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId,
const autoLoadAll = opts.autoLoadAll !== false;
const detailsContainer = document.getElementById('process-details-' + assistantMessageId);
const PAGE = PROCESS_DETAILS_PAGE_SIZE;
let offset = opts.append && detailsContainer && detailsContainer.dataset.nextOffset
const existingNextOffset = detailsContainer && detailsContainer.dataset.nextOffset
? parseInt(detailsContainer.dataset.nextOffset, 10) || 0
: 0;
const prepend = !!opts.prepend;
let offset = prepend && detailsContainer && detailsContainer.dataset.prevOffset
? parseInt(detailsContainer.dataset.prevOffset, 10) || 0
: opts.append && detailsContainer && detailsContainer.dataset.nextOffset
? parseInt(detailsContainer.dataset.nextOffset, 10) || 0
: 0;
const anchorId = opts.anchorId != null ? String(opts.anchorId).trim() : '';
let isFirst = !opts.append;
while (true) {
const params = new URLSearchParams();
params.set('limit', String(PAGE));
if (anchorId && !opts.append && !prepend) {
params.set('anchorId', anchorId);
} else {
params.set('offset', String(offset));
}
const res = await apiFetch(
'/api/messages/' + encodeURIComponent(String(backendMessageId)) +
'/process-details?limit=' + PAGE + '&offset=' + offset
'/process-details?' + params.toString()
);
const j = await res.json().catch(() => ({}));
if (!res.ok) {
@@ -1436,16 +1494,27 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId,
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
const hasMore = !!(j && j.hasMore);
renderProcessDetails(assistantMessageId, details, {
append: !isFirst,
append: !isFirst || opts.append,
prepend: prepend,
markLoaded: autoLoadAll ? !hasMore : true
});
offset += details.length;
const responseOffset = j && typeof j.offset === 'number' ? j.offset : offset;
const total = j && typeof j.total === 'number' ? j.total : responseOffset + details.length;
const nextOffset = prepend && existingNextOffset > 0
? existingNextOffset
: responseOffset + details.length;
const prevOffset = Math.max(0, responseOffset - PAGE);
offset = nextOffset;
if (detailsContainer) {
detailsContainer.dataset.lazyNotLoaded = '0';
detailsContainer.dataset.loaded = hasMore ? 'partial' : '1';
detailsContainer.dataset.nextOffset = String(offset);
detailsContainer.dataset.prevOffset = String(prevOffset);
detailsContainer.dataset.nextOffset = String(nextOffset);
}
updateProcessDetailsLoadMoreButton(assistantMessageId, backendMessageId, hasMore && !autoLoadAll);
updateProcessDetailsPaginationButtons(assistantMessageId, backendMessageId, {
hasPrev: !autoLoadAll && responseOffset > 0,
hasNext: !autoLoadAll && nextOffset < total
});
if (!hasMore || details.length === 0 || !autoLoadAll) {
break;
}
+63 -30
View File
@@ -167,18 +167,24 @@ function renderSkillsList() {
const fc = typeof skill.file_count === 'number' && skill.file_count > 0
? _t('skills.cardFiles', { count: skill.file_count })
: '';
const meta = [ver, fc, sc].filter(Boolean).join(' · ');
const metaItems = [ver, fc, sc].filter(Boolean);
const initial = (skill.name || sid || 'S').trim().charAt(0).toUpperCase();
return `
<div class="skill-card">
<div class="skill-card-header">
<h3 class="skill-card-title">${escapeHtml(skill.name || sid)}</h3>
${meta ? `<div class="skill-card-meta" style="opacity:0.85;font-size:12px;margin-top:4px;">${escapeHtml(meta)}</div>` : ''}
<div class="skill-card-description">${escapeHtml(skill.description || _t('skills.noDescription'))}</div>
</div>
<div class="skill-card-actions">
<button type="button" class="btn-secondary btn-small" data-skill-view="${escapeHtml(sid)}">${_t('common.view')}</button>
<button type="button" class="btn-secondary btn-small" data-skill-edit="${escapeHtml(sid)}">${_t('common.edit')}</button>
<button type="button" class="btn-secondary btn-small btn-danger" data-skill-delete="${escapeHtml(sid)}">${_t('common.delete')}</button>
<div class="skill-card-mark" aria-hidden="true">${escapeHtml(initial)}</div>
<div class="skill-card-body">
<div class="skill-card-header">
<div class="skill-card-title-row">
<h3 class="skill-card-title">${escapeHtml(skill.name || sid)}</h3>
${metaItems.length ? `<div class="skill-card-meta">${metaItems.map(item => `<span>${escapeHtml(item)}</span>`).join('')}</div>` : ''}
</div>
<div class="skill-card-description">${escapeHtml(skill.description || _t('skills.noDescription'))}</div>
</div>
<div class="skill-card-actions">
<button type="button" class="btn-secondary btn-small" data-skill-view="${escapeHtml(sid)}">${_t('common.view')}</button>
<button type="button" class="btn-secondary btn-small" data-skill-edit="${escapeHtml(sid)}">${_t('common.edit')}</button>
<button type="button" class="btn-secondary btn-small btn-danger" data-skill-delete="${escapeHtml(sid)}">${_t('common.delete')}</button>
</div>
</div>
</div>
`;
@@ -347,6 +353,27 @@ function updateSkillsManagementStats() {
if (totalEl) {
totalEl.textContent = skillsPagination.total;
}
const currentCountEl = document.getElementById('skills-current-count');
if (currentCountEl) {
currentCountEl.textContent = skillsList.length;
}
const fileCountEl = document.getElementById('skills-file-count');
if (fileCountEl) {
const fileCount = skillsList.reduce((sum, skill) => {
return sum + (typeof skill.file_count === 'number' ? skill.file_count : 0);
}, 0);
fileCountEl.textContent = fileCount;
}
const scriptCountEl = document.getElementById('skills-script-count');
if (scriptCountEl) {
const scriptCount = skillsList.reduce((sum, skill) => {
return sum + (typeof skill.script_count === 'number' ? skill.script_count : 0);
}, 0);
scriptCountEl.textContent = scriptCount;
}
}
// 搜索skills
@@ -920,23 +947,23 @@ function renderSkillsMonitor() {
: '0.0';
statsEl.innerHTML = `
<div class="monitor-stat-card">
<div class="monitor-stat-card skills-stat-card skills-stat-card--total">
<div class="monitor-stat-label">${_t('skills.totalSkillsCount')}</div>
<div class="monitor-stat-value">${skillsStats.total}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-card skills-stat-card skills-stat-card--calls">
<div class="monitor-stat-label">${_t('skills.totalCallsCount')}</div>
<div class="monitor-stat-value">${skillsStats.totalCalls}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-card skills-stat-card skills-stat-card--success">
<div class="monitor-stat-label">${_t('skills.successfulCalls')}</div>
<div class="monitor-stat-value" style="color: #28a745;">${skillsStats.totalSuccess}</div>
<div class="monitor-stat-value is-success">${skillsStats.totalSuccess}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-card skills-stat-card skills-stat-card--failed">
<div class="monitor-stat-label">${_t('skills.failedCalls')}</div>
<div class="monitor-stat-value" style="color: #dc3545;">${skillsStats.totalFailed}</div>
<div class="monitor-stat-value is-danger">${skillsStats.totalFailed}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-card skills-stat-card skills-stat-card--rate">
<div class="monitor-stat-label">${_t('skills.successRate')}</div>
<div class="monitor-stat-value">${successRate}%</div>
</div>
@@ -966,15 +993,15 @@ function renderSkillsMonitor() {
});
monitorListEl.innerHTML = `
<table class="monitor-table">
<table class="monitor-table skills-monitor-table">
<thead>
<tr>
<th style="text-align: left !important;">${_t('skills.skillName')}</th>
<th style="text-align: center;">${_t('skills.totalCalls')}</th>
<th style="text-align: center;">${_t('skills.success')}</th>
<th style="text-align: center;">${_t('skills.failure')}</th>
<th style="text-align: center;">${_t('skills.successRate')}</th>
<th style="text-align: left;">${_t('skills.lastCallTime')}</th>
<th>${_t('skills.skillName')}</th>
<th>${_t('skills.totalCalls')}</th>
<th>${_t('skills.success')}</th>
<th>${_t('skills.failure')}</th>
<th>${_t('skills.successRate')}</th>
<th>${_t('skills.lastCallTime')}</th>
</tr>
</thead>
<tbody>
@@ -984,15 +1011,21 @@ function renderSkillsMonitor() {
const failedCalls = stat.failed_calls || 0;
const successRate = totalCalls > 0 ? ((successCalls / totalCalls) * 100).toFixed(1) : '0.0';
const lastCallTime = stat.last_call_time && stat.last_call_time !== '-' ? stat.last_call_time : '-';
const rateValue = Math.max(0, Math.min(100, Number(successRate) || 0));
return `
<tr>
<td style="text-align: left !important;"><strong>${escapeHtml(stat.skill_name || '')}</strong></td>
<td style="text-align: center;">${totalCalls}</td>
<td style="text-align: center; color: #28a745; font-weight: 500;">${successCalls}</td>
<td style="text-align: center; color: #dc3545; font-weight: 500;">${failedCalls}</td>
<td style="text-align: center;">${successRate}%</td>
<td style="color: var(--text-secondary);">${escapeHtml(lastCallTime)}</td>
<td><strong>${escapeHtml(stat.skill_name || '')}</strong></td>
<td><span class="skills-table-pill">${totalCalls}</span></td>
<td><span class="skills-table-number is-success">${successCalls}</span></td>
<td><span class="skills-table-number is-danger">${failedCalls}</span></td>
<td>
<div class="skills-rate-cell">
<span>${successRate}%</span>
<span class="skills-rate-track"><span style="width: ${rateValue}%;"></span></span>
</div>
</td>
<td class="skills-last-call">${escapeHtml(lastCallTime)}</td>
</tr>
`;
}).join('')}