mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-06 19:08:41 +02:00
Add files via upload
This commit is contained in:
+23
-11
@@ -198,7 +198,7 @@ function renderSidebar() {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'api-group-item';
|
||||
const groupLabel = translateApiDocTag(group);
|
||||
li.innerHTML = `<a href="#" class="api-group-link" data-group="${escapeHtml(group)}">${escapeHtml(groupLabel)}</a>`;
|
||||
li.innerHTML = `<a href="#" class="api-group-link" data-group="${escapeAttr(group)}">${escapeHtml(groupLabel)}</a>`;
|
||||
groupList.appendChild(li);
|
||||
});
|
||||
|
||||
@@ -265,7 +265,7 @@ function createEndpointCard(endpoint) {
|
||||
<div class="api-endpoint-header">
|
||||
<div class="api-endpoint-title">
|
||||
<span class="api-method ${methodClass}">${endpoint.method.toUpperCase()}</span>
|
||||
<span class="api-path">${endpoint.path}</span>
|
||||
<span class="api-path">${escapeHtml(endpoint.path)}</span>
|
||||
${tagHtml}
|
||||
</div>
|
||||
</div>
|
||||
@@ -541,7 +541,7 @@ function renderTestSection(endpoint) {
|
||||
bodyInput = `
|
||||
<div class="api-test-input-group">
|
||||
<label>${escapeHtml(_t('apiDocs.requestBodyJson'))}</label>
|
||||
<textarea id="${bodyInputId}" class="test-body-input" placeholder='${escapeHtml(_t('apiDocs.requestBodyPlaceholder'))}'>${defaultBody}</textarea>
|
||||
<textarea id="${escapeAttr(bodyInputId)}" class="test-body-input" placeholder='${escapeAttr(_t('apiDocs.requestBodyPlaceholder'))}'>${escapeHtml(defaultBody)}</textarea>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -554,8 +554,8 @@ function renderTestSection(endpoint) {
|
||||
const inputId = `test-param-${param.name}-${escapeId(path)}-${method}`;
|
||||
return `
|
||||
<div class="api-test-input-group">
|
||||
<label>${param.name} <span style="color: var(--error-color);">*</span></label>
|
||||
<input type="text" id="${inputId}" placeholder="${param.description || param.name}" required>
|
||||
<label>${escapeHtml(param.name)} <span style="color: var(--error-color);">*</span></label>
|
||||
<input type="text" id="${escapeAttr(inputId)}" placeholder="${escapeAttr(param.description || param.name)}" required>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -572,11 +572,11 @@ function renderTestSection(endpoint) {
|
||||
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>
|
||||
<label>${escapeHtml(param.name)} ${required}</label>
|
||||
<input type="${param.schema?.type === 'number' || param.schema?.type === 'integer' ? 'number' : 'text'}"
|
||||
id="${inputId}"
|
||||
placeholder="${placeholder}"
|
||||
value="${defaultValue}"
|
||||
id="${escapeAttr(inputId)}"
|
||||
placeholder="${escapeAttr(placeholder)}"
|
||||
value="${escapeAttr(defaultValue)}"
|
||||
${param.required ? 'required' : ''}>
|
||||
</div>
|
||||
`;
|
||||
@@ -598,13 +598,13 @@ function renderTestSection(endpoint) {
|
||||
${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 || ''}')">
|
||||
<button class="api-test-btn primary" onclick="testAPI(${escapeJsStringAttr(method)}, ${escapeJsStringAttr(path)}, ${escapeJsStringAttr(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="${copyCurlTitle}">
|
||||
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, ${escapeJsStringAttr(method)}, ${escapeJsStringAttr(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"/>
|
||||
@@ -1006,6 +1006,18 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttr(escapeJsString(text));
|
||||
}
|
||||
|
||||
// ID转义(用于HTML ID属性)
|
||||
function escapeId(text) {
|
||||
return text.replace(/[{}]/g, '').replace(/\//g, '-');
|
||||
|
||||
@@ -11,6 +11,10 @@ function getAssetPageSize() {
|
||||
const assetPageState = { page: 1, pageSize: getAssetPageSize(), total: 0, totalPages: 1, items: [], projects: [], projectsLoaded: false, detailIndex: -1, editIndex: -1, detailAsset: null, editAsset: null, selected: new Map(), selectionQuery: '', allMatchingSelected: false, scanMode: 'chat', scanAssets: [], editorTags: [], editorDirty: false, editorBusy: false, editorReturnFocus: null, editorInteractionsReady: false, editorParsedTarget: '', importRows: [], importFileName: '', importBusy: false, importInteractionsReady: false, importReturnFocus: null };
|
||||
let assetOverviewDays = 30;
|
||||
|
||||
function assetEscapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
const ASSET_CUSTOM_SELECT_IDS = [
|
||||
'asset-status-filter',
|
||||
'asset-project-filter',
|
||||
@@ -1240,14 +1244,14 @@ function populateAssetProjectSelects() {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const current = el.value;
|
||||
el.innerHTML = `<option value="">${escapeHtml(emptyLabel)}</option>` + assetPageState.projects.map(project => `<option value="${escapeHtml(project.id)}">${escapeHtml(project.name)}${project.status === 'archived' ? ' · ' + escapeHtml(assetT('assets.archived', '已归档')) : ''}</option>`).join('');
|
||||
el.innerHTML = `<option value="">${escapeHtml(emptyLabel)}</option>` + assetPageState.projects.map(project => `<option value="${assetEscapeAttr(project.id)}">${escapeHtml(project.name)}${project.status === 'archived' ? ' · ' + escapeHtml(assetT('assets.archived', '已归档')) : ''}</option>`).join('');
|
||||
el.value = current;
|
||||
syncAssetSelect(el);
|
||||
});
|
||||
const batch = document.getElementById('asset-batch-project');
|
||||
if (batch) {
|
||||
const current = batch.value;
|
||||
batch.innerHTML = `<option value="" disabled hidden>${escapeHtml(assetT('assets.chooseProject', '请选择项目'))}</option>` + assetPageState.projects.map(project => `<option value="${escapeHtml(project.id)}">${escapeHtml(project.name)}${project.status === 'archived' ? ' · ' + escapeHtml(assetT('assets.archived', '已归档')) : ''}</option>`).join('');
|
||||
batch.innerHTML = `<option value="" disabled hidden>${escapeHtml(assetT('assets.chooseProject', '请选择项目'))}</option>` + assetPageState.projects.map(project => `<option value="${assetEscapeAttr(project.id)}">${escapeHtml(project.name)}${project.status === 'archived' ? ' · ' + escapeHtml(assetT('assets.archived', '已归档')) : ''}</option>`).join('');
|
||||
batch.value = current;
|
||||
syncAssetSelect(batch);
|
||||
}
|
||||
|
||||
+13
-1
@@ -632,6 +632,18 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttr(escapeJsString(text));
|
||||
}
|
||||
|
||||
/** @param {string} text @param {{ profile?: 'chat'|'timeline' }} [options] */
|
||||
function formatMarkdown(text, options) {
|
||||
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||
@@ -863,7 +875,7 @@ async function loadRobotAccountBindings() {
|
||||
<div class="robot-binding-account-name"><strong>${escapeHtml(platformLabels[binding.platform] || binding.platform || '-')}</strong><span>已连接</span></div>
|
||||
<small>账号标识 ${escapeHtml(binding.external_user_hint || '-')} · 更新于 ${escapeHtml(formatRobotBindingTime(binding.updated_at))}</small>
|
||||
</div>
|
||||
<button type="button" class="btn-secondary btn-small robot-binding-unbind-btn" onclick="deleteRobotAccountBinding('${escapeHtml(binding.id || '')}')">解除绑定</button>
|
||||
<button type="button" class="btn-secondary btn-small robot-binding-unbind-btn" onclick="deleteRobotAccountBinding(${escapeJsStringAttr(binding.id || '')})">解除绑定</button>
|
||||
</div>`).join('');
|
||||
if (typeof window.loadVulnerabilityAlertSubscription === 'function') {
|
||||
window.loadVulnerabilityAlertSubscription();
|
||||
|
||||
+164
-85
@@ -113,10 +113,10 @@
|
||||
if (!id || seen.has(id)) return;
|
||||
seen.add(id);
|
||||
const label = c2ProjectDisplayName(id, name);
|
||||
html += `<option value="${escapeHtml(id)}"${id === selected ? ' selected' : ''}>${escapeHtml(label)}</option>`;
|
||||
html += `<option value="${escapeAttr(id)}"${id === selected ? ' selected' : ''}>${escapeHtml(label)}</option>`;
|
||||
});
|
||||
if (selected && !seen.has(selected)) {
|
||||
html += `<option value="${escapeHtml(selected)}" selected>${escapeHtml(c2ProjectDisplayName(selected))}</option>`;
|
||||
html += `<option value="${escapeAttr(selected)}" selected>${escapeHtml(c2ProjectDisplayName(selected))}</option>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
@@ -147,7 +147,7 @@
|
||||
}
|
||||
|
||||
function c2ProjectBindSelectHtml(listener) {
|
||||
return `<select class="c2-project-bind-select" data-id="${escapeHtml(listener.id || '')}" title="${escapeHtml(c2t('assets.project') || '所属项目')}" onclick="event.stopPropagation()" onchange="C2.bindListenerProject(this.dataset.id, this.value)">${c2ProjectOptionsHtml(c2ResourceProjectId(listener))}</select>`;
|
||||
return `<select class="c2-project-bind-select" data-id="${escapeAttr(listener.id || '')}" title="${escapeAttr(c2t('assets.project') || '所属项目')}" onclick="event.stopPropagation()" onchange="C2.bindListenerProject(this.dataset.id, this.value)">${c2ProjectOptionsHtml(c2ResourceProjectId(listener))}</select>`;
|
||||
}
|
||||
|
||||
function withC2ProjectQuery(url) {
|
||||
@@ -478,7 +478,7 @@
|
||||
if (empty) valueClasses.push('is-empty');
|
||||
const rowCls = opts && opts.full ? ' c2-session-info-dl__row--full' : '';
|
||||
const copyBtn = (opts && opts.copy && !empty)
|
||||
? `<button type="button" class="c2-session-info-copy" title="${escapeHtml(c2t('c2.sessions.infoCopy'))}" aria-label="${escapeHtml(c2t('c2.sessions.infoCopy'))}" onclick="event.stopPropagation(); C2.copyText(${JSON.stringify(String(value))})">
|
||||
? `<button type="button" class="c2-session-info-copy" title="${escapeAttr(c2t('c2.sessions.infoCopy'))}" aria-label="${escapeAttr(c2t('c2.sessions.infoCopy'))}" data-c2-copy-value="${escapeAttr(String(value))}">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" stroke-width="1.8"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
|
||||
</button>`
|
||||
: '';
|
||||
@@ -486,7 +486,7 @@
|
||||
<div class="c2-session-info-dl__row${rowCls}">
|
||||
<dt class="c2-session-info-dl__label">${escapeHtml(label)}</dt>
|
||||
<dd class="${valueClasses.join(' ')}">
|
||||
<span class="c2-session-info-dl__text" title="${escapeHtml(empty ? '' : display)}">${escapeHtml(display)}</span>
|
||||
<span class="c2-session-info-dl__text" title="${escapeAttr(empty ? '' : display)}">${escapeHtml(display)}</span>
|
||||
${copyBtn}
|
||||
</dd>
|
||||
</div>`;
|
||||
@@ -543,16 +543,16 @@
|
||||
<div class="c2-session-info-block__head">
|
||||
<span class="c2-session-info-block__icon c2-session-info-block__icon--note" aria-hidden="true"></span>
|
||||
<span>${escapeHtml(c2t('c2.sessions.infoSectionNote'))}</span>
|
||||
<button type="button" class="c2-session-note-edit-btn" data-require-permission="c2:write" onclick='C2.beginEditSessionNote(${JSON.stringify(s.id)})'>${escapeHtml(c2t('c2.sessions.noteEdit'))}</button>
|
||||
<button type="button" class="c2-session-note-edit-btn" data-require-permission="c2:write" data-c2-action="session-note-edit" data-c2-id="${escapeAttr(s.id)}">${escapeHtml(c2t('c2.sessions.noteEdit'))}</button>
|
||||
</div>
|
||||
<div class="c2-session-info-note${noteEmpty ? ' is-empty' : ''}" id="c2-session-note-view">${escapeHtml(noteText || c2t('c2.sessions.infoNoteEmpty'))}</div>
|
||||
<div class="c2-session-note-editor" id="c2-session-note-editor" hidden>
|
||||
<textarea id="c2-session-note-input" class="c2-session-note-textarea" maxlength="2000" rows="4" placeholder="${escapeHtml(c2t('c2.sessions.notePlaceholder'))}">${escapeHtml(noteText)}</textarea>
|
||||
<textarea id="c2-session-note-input" class="c2-session-note-textarea" maxlength="2000" rows="4" placeholder="${escapeAttr(c2t('c2.sessions.notePlaceholder'))}">${escapeHtml(noteText)}</textarea>
|
||||
<div class="c2-session-note-editor__footer">
|
||||
<span class="c2-session-note-counter" id="c2-session-note-counter">${noteText.length}/2000</span>
|
||||
<div class="c2-session-note-editor__actions">
|
||||
<button type="button" class="btn-secondary btn-sm" onclick="C2.cancelEditSessionNote()">${escapeHtml(c2t('common.cancel'))}</button>
|
||||
<button type="button" class="btn-primary btn-sm" id="c2-session-note-save" onclick='C2.saveSessionNote(${JSON.stringify(s.id)})'>${escapeHtml(c2t('common.save'))}</button>
|
||||
<button type="button" class="btn-primary btn-sm" id="c2-session-note-save" data-c2-action="session-note-save" data-c2-id="${escapeAttr(s.id)}">${escapeHtml(c2t('common.save'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -638,15 +638,19 @@
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/** 任务列表操作按钮(查看/取消/删除)— 事件委托 */
|
||||
function bindC2TaskActionDelegation() {
|
||||
if (document.documentElement.dataset.c2TaskActionsBound === '1') return;
|
||||
document.documentElement.dataset.c2TaskActionsBound = '1';
|
||||
document.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('[data-c2-task-action]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const btn = e.target.closest('[data-c2-task-action]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const action = btn.getAttribute('data-c2-task-action');
|
||||
const id = btn.getAttribute('data-task-id');
|
||||
if (!id) return;
|
||||
@@ -657,6 +661,75 @@
|
||||
}
|
||||
bindC2TaskActionDelegation();
|
||||
|
||||
/** C2 动态内容操作按钮 — 避免把用户可控值拼入 inline onclick */
|
||||
function bindC2SafeActionDelegation() {
|
||||
if (document.documentElement.dataset.c2SafeActionsBound === '1') return;
|
||||
document.documentElement.dataset.c2SafeActionsBound = '1';
|
||||
document.addEventListener('click', function(e) {
|
||||
const copyBtn = e.target.closest('[data-c2-copy-value]');
|
||||
if (copyBtn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
C2.copyText(copyBtn.getAttribute('data-c2-copy-value') || '');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileBtn = e.target.closest('[data-c2-file-action]');
|
||||
if (fileBtn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const name = fileBtn.getAttribute('data-c2-file-name') || '';
|
||||
const action = fileBtn.getAttribute('data-c2-file-action');
|
||||
if (action === 'open') C2.openDirectory(name);
|
||||
else if (action === 'download') C2.downloadFile(name);
|
||||
return;
|
||||
}
|
||||
|
||||
const stopEl = e.target.closest('[data-c2-stop-action]');
|
||||
const actionEl = e.target.closest('[data-c2-action]');
|
||||
if (stopEl && (!actionEl || !stopEl.contains(actionEl))) return;
|
||||
if (!actionEl) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
runC2SafeAction(actionEl);
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||
const actionEl = e.target.closest('[data-c2-action][role="button"]');
|
||||
if (!actionEl) return;
|
||||
e.preventDefault();
|
||||
runC2SafeAction(actionEl);
|
||||
});
|
||||
}
|
||||
bindC2SafeActionDelegation();
|
||||
|
||||
function runC2SafeAction(el) {
|
||||
const action = el.getAttribute('data-c2-action');
|
||||
const id = el.getAttribute('data-c2-id') || '';
|
||||
switch (action) {
|
||||
case 'session-note-edit': C2.beginEditSessionNote(id); break;
|
||||
case 'session-note-save': C2.saveSessionNote(id); break;
|
||||
case 'listener-start': C2.startListener(id); break;
|
||||
case 'listener-stop': C2.stopListener(id); break;
|
||||
case 'listener-edit': C2.editListener(id); break;
|
||||
case 'listener-delete': C2.deleteListener(id); break;
|
||||
case 'listener-save': C2.saveListener(id); break;
|
||||
case 'session-select': C2.selectSession(id); break;
|
||||
case 'session-delete': C2.deleteSessionRecord(id); break;
|
||||
case 'session-sleep': C2.setSessionSleep(id); break;
|
||||
case 'session-kill': C2.killSession(id); break;
|
||||
case 'session-tasks-refresh': C2.loadSessionTasks(id); break;
|
||||
case 'task-view': C2.viewTask(id); break;
|
||||
case 'event-view': C2.viewEvent(id); break;
|
||||
case 'event-delete': C2.deleteEventById(id); break;
|
||||
case 'profile-delete': C2.deleteProfile(id); break;
|
||||
case 'payload-download': {
|
||||
if (typeof window.__c2DownloadPayload === 'function') window.__c2DownloadPayload(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听器表单:Malleable Profile 下拉选项 HTML(value / 文本已转义) */
|
||||
function listenerProfileSelectHtml(selectedProfileId) {
|
||||
const sel = selectedProfileId ? String(selectedProfileId) : '';
|
||||
@@ -665,7 +738,7 @@
|
||||
if (!p) continue;
|
||||
const pid = p.id || p.ID;
|
||||
if (!pid) continue;
|
||||
const idEsc = escapeHtml(String(pid));
|
||||
const idEsc = escapeAttr(String(pid));
|
||||
const nameEsc = escapeHtml(p.name || pid);
|
||||
const selected = sel && String(pid) === sel ? ' selected' : '';
|
||||
opts += `<option value="${idEsc}"${selected}>${nameEsc}</option>`;
|
||||
@@ -852,7 +925,7 @@
|
||||
const profilePid = listenerResolvedProfileId(l);
|
||||
const profileName = listenerProfileDisplayName(l);
|
||||
const profileBadge = profilePid
|
||||
? '<div class="c2-listener-profile-badge" title="' + escapeHtml(c2t('c2.listeners.profileBadgeTitle')) + '"><span class="c2-listener-profile-dot" aria-hidden="true"></span><span>' + escapeHtml(profileName) + '</span></div>'
|
||||
? '<div class="c2-listener-profile-badge" title="' + escapeAttr(c2t('c2.listeners.profileBadgeTitle')) + '"><span class="c2-listener-profile-dot" aria-hidden="true"></span><span>' + escapeHtml(profileName) + '</span></div>'
|
||||
: '';
|
||||
const cb = C2.getListenerCallbackHost(l);
|
||||
const cbRow = cb
|
||||
@@ -868,7 +941,7 @@
|
||||
const bindVal = escapeHtml(String(l.bindHost)) + ':' + escapeHtml(String(l.bindPort));
|
||||
|
||||
return `
|
||||
<article class="c2-listener-card c2-listener-card--${stUi}" data-listener-id="${escapeHtml(l.id)}">
|
||||
<article class="c2-listener-card c2-listener-card--${stUi}" data-listener-id="${escapeAttr(l.id)}">
|
||||
<div class="c2-listener-card-head">
|
||||
<div class="c2-ltype-mark ${typeVis}" title="${fullType}"><span>${typeMark}</span></div>
|
||||
<div class="c2-listener-card-head-main">
|
||||
@@ -877,7 +950,7 @@
|
||||
<span class="c2-listener-pill c2-listener-pill--${stUi}">${pillLabel}</span>
|
||||
</div>
|
||||
<div class="c2-listener-id-row">
|
||||
<code class="c2-listener-id-full" title="${escapeHtml(l.id)}">${escapeHtml(l.id)}</code>
|
||||
<code class="c2-listener-id-full" title="${escapeAttr(l.id)}">${escapeHtml(l.id)}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -894,11 +967,11 @@
|
||||
</div>
|
||||
<div class="c2-listener-card-actions">
|
||||
${l.status === 'stopped'
|
||||
? `<button type="button" class="btn-primary btn-sm" data-require-permission="c2:write" onclick="C2.startListener('${l.id}')">▶ ${escapeHtml(c2t('c2.listeners.start'))}</button>`
|
||||
: `<button type="button" class="btn-secondary btn-sm" data-require-permission="c2:write" onclick="C2.stopListener('${l.id}')">⏹ ${escapeHtml(c2t('c2.listeners.stop'))}</button>`
|
||||
? `<button type="button" class="btn-primary btn-sm" data-require-permission="c2:write" data-c2-action="listener-start" data-c2-id="${escapeAttr(l.id)}">▶ ${escapeHtml(c2t('c2.listeners.start'))}</button>`
|
||||
: `<button type="button" class="btn-secondary btn-sm" data-require-permission="c2:write" data-c2-action="listener-stop" data-c2-id="${escapeAttr(l.id)}">⏹ ${escapeHtml(c2t('c2.listeners.stop'))}</button>`
|
||||
}
|
||||
<button type="button" class="btn-secondary btn-sm" data-require-permission="c2:write" onclick="C2.editListener('${l.id}')">${escapeHtml(c2t('c2.listeners.edit'))}</button>
|
||||
<button type="button" class="btn-danger btn-sm" data-require-permission="c2:delete" onclick="C2.deleteListener('${l.id}')">${escapeHtml(c2t('c2.listeners.delete'))}</button>
|
||||
<button type="button" class="btn-secondary btn-sm" data-require-permission="c2:write" data-c2-action="listener-edit" data-c2-id="${escapeAttr(l.id)}">${escapeHtml(c2t('c2.listeners.edit'))}</button>
|
||||
<button type="button" class="btn-danger btn-sm" data-require-permission="c2:delete" data-c2-action="listener-delete" data-c2-id="${escapeAttr(l.id)}">${escapeHtml(c2t('c2.listeners.delete'))}</button>
|
||||
</div>
|
||||
</article>`;
|
||||
}).join('');
|
||||
@@ -963,7 +1036,7 @@
|
||||
<div class="c2-form-row">
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
|
||||
<input type="text" id="c2-listener-name" class="form-control" placeholder="${escapeHtml(c2t('c2.listeners.placeholderNameExample'))}">
|
||||
<input type="text" id="c2-listener-name" class="form-control" placeholder="${escapeAttr(c2t('c2.listeners.placeholderNameExample'))}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.type'))}</label>
|
||||
@@ -1003,7 +1076,7 @@
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.remark'))}</label>
|
||||
<input type="text" id="c2-listener-remark" class="form-control" placeholder="${escapeHtml(c2t('c2.listeners.placeholderRemarkLong'))}">
|
||||
<input type="text" id="c2-listener-remark" class="form-control" placeholder="${escapeAttr(c2t('c2.listeners.placeholderRemarkLong'))}">
|
||||
</div>
|
||||
<div class="c2-form-group" id="c2-listener-legacy-shell-group" style="display:none;">
|
||||
<label class="c2-checkbox-label">
|
||||
@@ -1191,7 +1264,7 @@
|
||||
<div class="c2-modal-body">
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
|
||||
<input type="text" id="c2-listener-name" class="form-control" value="${escapeHtml(l.name)}">
|
||||
<input type="text" id="c2-listener-name" class="form-control" value="${escapeAttr(l.name)}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('assets.project') || '所属项目')}</label>
|
||||
@@ -1200,7 +1273,7 @@
|
||||
<div class="c2-form-row">
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.bindHost'))}</label>
|
||||
<input type="text" id="c2-listener-host" class="form-control" value="${escapeHtml(String(l.bindHost))}">
|
||||
<input type="text" id="c2-listener-host" class="form-control" value="${escapeAttr(String(l.bindHost))}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
|
||||
@@ -1215,12 +1288,12 @@
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.callbackHost'))}</label>
|
||||
<input type="text" id="c2-listener-callback-host" class="form-control" value="${escapeHtml(cbHost)}">
|
||||
<input type="text" id="c2-listener-callback-host" class="form-control" value="${escapeAttr(cbHost)}">
|
||||
<div class="form-hint">${escapeHtml(c2t('c2.listeners.callbackHostHint'))}</div>
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.remark'))}</label>
|
||||
<input type="text" id="c2-listener-remark" class="form-control" value="${escapeHtml(l.remark || '')}">
|
||||
<input type="text" id="c2-listener-remark" class="form-control" value="${escapeAttr(l.remark || '')}">
|
||||
</div>
|
||||
${lt === 'tcp_reverse' ? `
|
||||
<div class="c2-form-group" id="c2-listener-legacy-shell-group">
|
||||
@@ -1233,7 +1306,7 @@
|
||||
</div>
|
||||
<div class="c2-modal-footer">
|
||||
<button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.cancel'))}</button>
|
||||
<button class="btn-primary" onclick="C2.saveListener('${l.id}')">${escapeHtml(c2t('common.save'))}</button>
|
||||
<button class="btn-primary" data-c2-action="listener-save" data-c2-id="${escapeAttr(l.id)}">${escapeHtml(c2t('common.save'))}</button>
|
||||
</div>
|
||||
`;
|
||||
C2.refreshFormSelects(content);
|
||||
@@ -1304,11 +1377,11 @@
|
||||
const listenerOpts = ['<option value="">' + escapeHtml(c2t('c2.sessions.filterAllListeners')) + '</option>']
|
||||
.concat(listeners.map(l => {
|
||||
const sel = f.listener_id === l.id ? ' selected' : '';
|
||||
return `<option value="${escapeHtml(l.id)}"${sel}>${escapeHtml(l.name)}</option>`;
|
||||
return `<option value="${escapeAttr(l.id)}"${sel}>${escapeHtml(l.name)}</option>`;
|
||||
})).join('');
|
||||
toolbar.innerHTML = `
|
||||
<div class="c2-sessions-filter-row">
|
||||
<select id="c2-session-filter-status" class="form-control c2-native-select" title="${escapeHtml(c2t('c2.sessions.status'))}" onchange="C2.applySessionFilter()">
|
||||
<select id="c2-session-filter-status" class="form-control c2-native-select" title="${escapeAttr(c2t('c2.sessions.status'))}" onchange="C2.applySessionFilter()">
|
||||
<option value="">${escapeHtml(c2t('c2.sessions.filterAllStatus'))}</option>
|
||||
<option value="active"${f.status === 'active' ? ' selected' : ''}>${escapeHtml(c2t('c2.sessions.active'))}</option>
|
||||
<option value="sleeping"${f.status === 'sleeping' ? ' selected' : ''}>${escapeHtml(c2t('c2.sessions.sleeping'))}</option>
|
||||
@@ -1316,7 +1389,7 @@
|
||||
</select>
|
||||
<select id="c2-session-filter-listener" class="form-control c2-native-select" onchange="C2.applySessionFilter()">${listenerOpts}</select>
|
||||
</div>
|
||||
<input type="text" id="c2-session-filter-search" class="form-control" placeholder="${escapeHtml(c2t('c2.sessions.filterSearchPlaceholder'))}" value="${escapeHtml(f.search || '')}" onkeydown="if(event.key==='Enter'){C2.applySessionFilter();}">
|
||||
<input type="text" id="c2-session-filter-search" class="form-control" placeholder="${escapeAttr(c2t('c2.sessions.filterSearchPlaceholder'))}" value="${escapeAttr(f.search || '')}" onkeydown="if(event.key==='Enter'){C2.applySessionFilter();}">
|
||||
<div class="c2-sessions-toolbar-meta">
|
||||
<label class="c2-sessions-select-all-label">
|
||||
<input type="checkbox" id="c2-sessions-select-all" onchange="C2.onSessionsSelectAll(this.checked)">
|
||||
@@ -1452,10 +1525,13 @@
|
||||
const osEmpty = isEmptyInfoValue(s.os) && isEmptyInfoValue(s.arch);
|
||||
return `
|
||||
<div class="c2-session-item ${s.id === C2.selectedSessionId ? 'active' : ''}"
|
||||
data-status="${escapeHtml(s.status || '')}"
|
||||
onclick="C2.selectSession('${s.id}')">
|
||||
<input type="checkbox" class="c2-session-item-check c2-session-row-check" data-id="${escapeHtml(s.id)}"
|
||||
onclick="event.stopPropagation();" onchange="C2.syncSessionsToolbar()">
|
||||
data-status="${escapeAttr(s.status || '')}"
|
||||
data-c2-action="session-select"
|
||||
data-c2-id="${escapeAttr(s.id)}"
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<input type="checkbox" class="c2-session-item-check c2-session-row-check" data-id="${escapeAttr(s.id)}"
|
||||
data-c2-stop-action="1" onchange="C2.syncSessionsToolbar()">
|
||||
<div class="c2-session-item-body">
|
||||
<div class="c2-session-header">
|
||||
<div class="c2-session-host-row">
|
||||
@@ -1474,8 +1550,8 @@
|
||||
<span class="c2-session-chip c2-session-chip--dim">PID ${escapeHtml(String(s.pid != null ? s.pid : '—'))}</span>
|
||||
</div>
|
||||
<div class="c2-session-item-footer">
|
||||
<span class="c2-session-meta c2-session-item-time" title="${escapeHtml(formatTime(s.lastCheckIn))}">${escapeHtml(formatRelativeTime(s.lastCheckIn) || formatTime(s.lastCheckIn))}</span>
|
||||
<button type="button" class="c2-session-card-delete" data-require-permission="c2:delete" onclick="event.stopPropagation(); C2.deleteSessionRecord('${s.id}');">${escapeHtml(c2t('c2.sessions.cardDeleteSession'))}</button>
|
||||
<span class="c2-session-meta c2-session-item-time" title="${escapeAttr(formatTime(s.lastCheckIn))}">${escapeHtml(formatRelativeTime(s.lastCheckIn) || formatTime(s.lastCheckIn))}</span>
|
||||
<button type="button" class="c2-session-card-delete" data-require-permission="c2:delete" data-c2-action="session-delete" data-c2-id="${escapeAttr(s.id)}">${escapeHtml(c2t('c2.sessions.cardDeleteSession'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -1531,7 +1607,7 @@
|
||||
</div>
|
||||
<div class="c2-session-hero__sub${isEmptyInfoValue(s.username) && isEmptyInfoValue(s.os) ? ' is-muted' : ''}">${escapeHtml(sessionMetaLine(s))}</div>
|
||||
<div class="c2-session-hero__chips">
|
||||
<span class="c2-session-hero-chip is-mono" title="${escapeHtml(c2t('c2.sessions.infoSessionId'))}">${escapeHtml(s.id)}</span>
|
||||
<span class="c2-session-hero-chip is-mono" title="${escapeAttr(c2t('c2.sessions.infoSessionId'))}">${escapeHtml(s.id)}</span>
|
||||
<span class="c2-session-hero-chip">${escapeHtml(s.internalIp || '—')}</span>
|
||||
<span class="c2-session-hero-chip">PID ${escapeHtml(String(s.pid != null ? s.pid : '—'))}</span>
|
||||
</div>
|
||||
@@ -1544,8 +1620,8 @@
|
||||
<span class="c2-session-hero__heartbeat-value">${escapeHtml(heartbeatRel)}</span>
|
||||
</div>
|
||||
<div class="c2-session-actions">
|
||||
<button class="btn-secondary btn-sm" data-require-permission="c2:write" onclick="C2.setSessionSleep('${s.id}')">${escapeHtml(c2t('c2.sessions.btnSleep'))}</button>
|
||||
<button class="btn-danger btn-sm" data-require-permission="c2:write" onclick="C2.killSession('${s.id}')">${escapeHtml(c2t('c2.sessions.kill'))}</button>
|
||||
<button class="btn-secondary btn-sm" data-require-permission="c2:write" data-c2-action="session-sleep" data-c2-id="${escapeAttr(s.id)}">${escapeHtml(c2t('c2.sessions.btnSleep'))}</button>
|
||||
<button class="btn-danger btn-sm" data-require-permission="c2:write" data-c2-action="session-kill" data-c2-id="${escapeAttr(s.id)}">${escapeHtml(c2t('c2.sessions.kill'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1571,7 +1647,7 @@
|
||||
<div class="c2-file-toolbar">
|
||||
<button class="btn-ghost btn-sm" onclick="C2.goToParentDirectory()">${escapeHtml(c2t('c2.files.parent'))}</button>
|
||||
<button class="btn-ghost btn-sm" onclick="C2.refreshFiles()">${escapeHtml(c2t('c2.files.refresh'))}</button>
|
||||
<button type="button" class="btn-ghost btn-sm" id="c2-file-upload-btn" data-require-permission="c2:write" onclick="C2.openFileUploadPicker()" title="${escapeHtml(c2t('c2.files.upload'))}">${escapeHtml(c2t('c2.files.upload'))}</button>
|
||||
<button type="button" class="btn-ghost btn-sm" id="c2-file-upload-btn" data-require-permission="c2:write" onclick="C2.openFileUploadPicker()" title="${escapeAttr(c2t('c2.files.upload'))}">${escapeHtml(c2t('c2.files.upload'))}</button>
|
||||
<input type="file" id="c2-file-upload-input" style="display:none" onchange="C2.onC2FileUploadPick(event)" />
|
||||
<span id="c2-current-path" class="c2-path-breadcrumb">/</span>
|
||||
</div>
|
||||
@@ -1698,7 +1774,7 @@
|
||||
<p class="c2-sleep-modal__host">${escapeHtml(hostLabel)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="c2-modal-close" onclick="C2.closeModal()" aria-label="${escapeHtml(c2t('common.close'))}">×</button>
|
||||
<button type="button" class="c2-modal-close" onclick="C2.closeModal()" aria-label="${escapeAttr(c2t('common.close'))}">×</button>
|
||||
</div>
|
||||
<div class="c2-sleep-modal__current">${escapeHtml(currentLine)}</div>
|
||||
<div class="c2-sleep-modal__body">
|
||||
@@ -2778,16 +2854,16 @@
|
||||
<td>
|
||||
<div class="c2-file-name">
|
||||
<span class="${iconCls}" aria-hidden="true"></span>
|
||||
<span class="c2-file-name-text" title="${escapeHtml(entry.name)}">${escapeHtml(entry.name)}</span>
|
||||
<span class="c2-file-name-text" title="${escapeAttr(entry.name)}">${escapeHtml(entry.name)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td title="${escapeHtml(String(entry.size || ''))}">${escapeHtml(sizeLabel)}</td>
|
||||
<td title="${escapeAttr(String(entry.size || ''))}">${escapeHtml(sizeLabel)}</td>
|
||||
<td>${escapeHtml(entry.mode)}</td>
|
||||
<td>
|
||||
${entry.isDir
|
||||
? `<button class="btn-ghost btn-sm c2-file-action-btn" onclick='C2.openDirectory(${JSON.stringify(entry.name)})'>${escapeHtml(c2t('c2.files.open'))}</button>`
|
||||
: `<button class="btn-ghost btn-sm c2-file-action-btn" onclick='C2.downloadFile(${JSON.stringify(entry.name)})'>${escapeHtml(c2t('c2.files.download'))}</button>`
|
||||
}
|
||||
<button type="button"
|
||||
class="btn-ghost btn-sm c2-file-action-btn"
|
||||
data-c2-file-action="${entry.isDir ? 'open' : 'download'}"
|
||||
data-c2-file-name="${escapeAttr(entry.name)}">${escapeHtml(c2t(entry.isDir ? 'c2.files.open' : 'c2.files.download'))}</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -3366,7 +3442,7 @@
|
||||
<span class="c2-session-tasks-heading">${escapeHtml(c2t('c2.tasks.sessionTaskHistory'))}</span>
|
||||
<span class="c2-session-tasks-count">0</span>
|
||||
</div>
|
||||
<button type="button" class="btn-ghost btn-sm c2-session-tasks-refresh" onclick="C2.loadSessionTasks('${escapeHtml(sessionId)}')">${refreshBtn}</button>
|
||||
<button type="button" class="btn-ghost btn-sm c2-session-tasks-refresh" data-c2-action="session-tasks-refresh" data-c2-id="${escapeAttr(sessionId)}">${refreshBtn}</button>
|
||||
</div>
|
||||
<div class="c2-empty-inline">
|
||||
<div class="c2-empty-inline__icon" aria-hidden="true"></div>
|
||||
@@ -3383,7 +3459,7 @@
|
||||
<span class="c2-session-tasks-heading">${escapeHtml(c2t('c2.tasks.sessionTaskHistory'))}</span>
|
||||
<span class="c2-session-tasks-count">${countLabel}</span>
|
||||
</div>
|
||||
<button type="button" class="btn-ghost btn-sm c2-session-tasks-refresh" onclick="C2.loadSessionTasks('${escapeHtml(sessionId)}')">${refreshBtn}</button>
|
||||
<button type="button" class="btn-ghost btn-sm c2-session-tasks-refresh" data-c2-action="session-tasks-refresh" data-c2-id="${escapeAttr(sessionId)}">${refreshBtn}</button>
|
||||
</div>
|
||||
<div class="c2-session-tasks-rows">
|
||||
${tasks.map(t => {
|
||||
@@ -3395,11 +3471,11 @@
|
||||
const isPending = status === 'queued' || status === 'sent' || status === 'running';
|
||||
const timeStr = formatTime(t.completedAt || t.createdAt);
|
||||
return `
|
||||
<div class="c2-session-task-row ${isPending ? 'is-pending' : ''}" data-status="${escapeHtml(status)}">
|
||||
<div class="c2-session-task-row ${isPending ? 'is-pending' : ''}" data-status="${escapeAttr(status)}">
|
||||
<div class="c2-session-task-row__main">
|
||||
<span class="c2-task-status-dot ${escapeHtml(status)}" title="${escapeHtml(taskStatusLabel(status))}"></span>
|
||||
<span class="c2-task-status-dot ${escapeAttr(status)}" title="${escapeAttr(taskStatusLabel(status))}"></span>
|
||||
<span class="c2-task-type-badge c2-task-type-badge--${typeCat}">${escapeHtml(t.taskType || '-')}</span>
|
||||
<div class="c2-session-task-row__cmd" title="${escapeHtml(cmd || '')}">
|
||||
<div class="c2-session-task-row__cmd" title="${escapeAttr(cmd || '')}">
|
||||
${cmdShort
|
||||
? `<code class="c2-session-task-command">${escapeHtml(cmdShort)}</code>`
|
||||
: `<span class="c2-session-task-command c2-session-task-command--muted">—</span>`}
|
||||
@@ -3408,8 +3484,8 @@
|
||||
<div class="c2-session-task-row__meta">
|
||||
<span class="c2-status-badge ${escapeHtml(status)}">${escapeHtml(taskStatusLabel(status))}</span>
|
||||
<span class="c2-session-task-duration">${formatDuration(t.durationMs)}</span>
|
||||
<span class="c2-session-task-time" title="${escapeHtml(timeStr)}">${escapeHtml(formatRelativeTime(t.completedAt || t.createdAt) || timeStr)}</span>
|
||||
<button type="button" class="btn-secondary btn-small c2-session-task-view" data-c2-task-action="view" data-task-id="${escapeHtml(rawId)}">${escapeHtml(c2t('c2.tasks.view'))}</button>
|
||||
<span class="c2-session-task-time" title="${escapeAttr(timeStr)}">${escapeHtml(formatRelativeTime(t.completedAt || t.createdAt) || timeStr)}</span>
|
||||
<button type="button" class="btn-secondary btn-small c2-session-task-view" data-c2-task-action="view" data-task-id="${escapeAttr(rawId)}">${escapeHtml(c2t('c2.tasks.view'))}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
@@ -3446,7 +3522,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="c2-tasks-table-col-check">
|
||||
<label class="c2-task-check-label" title="${escapeHtml(c2t('c2.tasks.selectAll'))}">
|
||||
<label class="c2-task-check-label" title="${escapeAttr(c2t('c2.tasks.selectAll'))}">
|
||||
<input type="checkbox" id="c2-tasks-select-all" onchange="C2.onTasksSelectAll(this.checked)">
|
||||
</label>
|
||||
</th>
|
||||
@@ -3461,10 +3537,11 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${C2.tasks.map(t => {
|
||||
const rawId = t.id || '';
|
||||
const tid = escapeHtml(rawId);
|
||||
const status = String(t.status || '');
|
||||
${C2.tasks.map(t => {
|
||||
const rawId = t.id || '';
|
||||
const tid = escapeHtml(rawId);
|
||||
const tidAttr = escapeAttr(rawId);
|
||||
const status = String(t.status || '');
|
||||
const rowStatus = escapeHtml(status || 'queued');
|
||||
const typeCat = taskTypeCategory(t.taskType);
|
||||
const sessionFull = t.sessionId ? String(t.sessionId) : '';
|
||||
@@ -3478,25 +3555,25 @@
|
||||
const cmdEsc = escapeHtml(cmd);
|
||||
const canCancel = status === 'queued' || status === 'sent';
|
||||
return `
|
||||
<tr class="c2-tasks-row c2-tasks-row--${rowStatus}" data-task-id="${tid}" onclick="C2.viewTask('${tid}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();C2.viewTask('${tid}')}" role="button" tabindex="0">
|
||||
<td class="c2-tasks-table-col-check" onclick="event.stopPropagation();">
|
||||
<tr class="c2-tasks-row c2-tasks-row--${rowStatus}" data-task-id="${tidAttr}" data-c2-action="task-view" data-c2-id="${tidAttr}" role="button" tabindex="0">
|
||||
<td class="c2-tasks-table-col-check" data-c2-stop-action="1">
|
||||
<label class="c2-task-check-label">
|
||||
<input type="checkbox" class="c2-task-row-check" data-id="${tid}" onchange="C2.syncTasksToolbar()">
|
||||
<input type="checkbox" class="c2-task-row-check" data-id="${tidAttr}" onchange="C2.syncTasksToolbar()">
|
||||
</label>
|
||||
</td>
|
||||
<td class="c2-tasks-col-time">${escapeHtml(formatTime(t.createdAt))}</td>
|
||||
<td><span class="c2-status-badge ${escapeHtml(status)}">${escapeHtml(taskStatusLabel(status))}</span></td>
|
||||
<td><span class="c2-task-type-badge c2-task-type-badge--${typeCat}">${escapeHtml(t.taskType || '-')}</span></td>
|
||||
<td class="c2-tasks-col-command" title="${cmdEsc}">${cmdEsc || dash}</td>
|
||||
<td class="c2-tasks-col-mono" title="${escapeHtml(sessionFull)}">${sessionShort || dash}</td>
|
||||
<td class="c2-tasks-col-mono" title="${tid}">${taskShort || dash}</td>
|
||||
<td class="c2-tasks-col-command" title="${escapeAttr(cmd)}">${cmdEsc || dash}</td>
|
||||
<td class="c2-tasks-col-mono" title="${escapeAttr(sessionFull)}">${sessionShort || dash}</td>
|
||||
<td class="c2-tasks-col-mono" title="${tidAttr}">${taskShort || dash}</td>
|
||||
<td class="c2-tasks-col-duration">${formatDuration(t.durationMs)}</td>
|
||||
<td class="c2-tasks-table-col-actions" onclick="event.stopPropagation();">
|
||||
<td class="c2-tasks-table-col-actions" data-c2-stop-action="1">
|
||||
<div class="c2-tasks-row-actions">
|
||||
${canCancel
|
||||
? `<button type="button" class="c2-tasks-cancel-btn" data-c2-task-action="cancel" data-task-id="${tid}" title="${cancelTitle}" aria-label="${cancelTitle}">${escapeHtml(c2t('c2.tasks.cancelBtn'))}</button>`
|
||||
: ''}
|
||||
<button type="button" class="c2-tasks-delete-btn" data-require-permission="c2:delete" data-c2-task-action="delete" data-task-id="${tid}" title="${delTitle}" aria-label="${delTitle}">${deleteIcon}</button>
|
||||
? `<button type="button" class="c2-tasks-cancel-btn" data-c2-task-action="cancel" data-task-id="${tidAttr}" title="${cancelTitle}" aria-label="${cancelTitle}">${escapeHtml(c2t('c2.tasks.cancelBtn'))}</button>`
|
||||
: ''}
|
||||
<button type="button" class="c2-tasks-delete-btn" data-require-permission="c2:delete" data-c2-task-action="delete" data-task-id="${tidAttr}" title="${delTitle}" aria-label="${delTitle}">${deleteIcon}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -3717,7 +3794,7 @@
|
||||
C2.renderPayloadPage = function() {
|
||||
const optionsHtml = C2.listeners.length > 0
|
||||
? C2.listeners.map(l =>
|
||||
`<option value="${l.id}">${escapeHtml(l.name)} (${l.type} ${l.bindHost}:${l.bindPort})</option>`
|
||||
`<option value="${escapeAttr(l.id)}">${escapeHtml(l.name)} (${escapeHtml(l.type)} ${escapeHtml(l.bindHost)}:${escapeHtml(l.bindPort)})</option>`
|
||||
).join('')
|
||||
: '<option value="">' + escapeHtml(c2t('c2.payloads.noListenersOption')) + '</option>';
|
||||
|
||||
@@ -3734,7 +3811,7 @@
|
||||
let buildOptionsHtml;
|
||||
if (listeners.length > 0) {
|
||||
buildOptionsHtml = listeners.map(l =>
|
||||
`<option value="${l.id}">${escapeHtml(l.name)} (${l.type} ${l.bindHost}:${l.bindPort})</option>`
|
||||
`<option value="${escapeAttr(l.id)}">${escapeHtml(l.name)} (${escapeHtml(l.type)} ${escapeHtml(l.bindHost)}:${escapeHtml(l.bindPort)})</option>`
|
||||
).join('');
|
||||
} else {
|
||||
buildOptionsHtml = '<option value="">' + escapeHtml(c2t('c2.payloads.noListenersOption')) + '</option>';
|
||||
@@ -3831,7 +3908,7 @@
|
||||
<div>✓ ${escapeHtml(c2t('c2.payloads.buildSuccessTitle'))}</div>
|
||||
<div>${escapeHtml(c2t('c2.payloads.buildMetaOsArch', { os: data.payload?.os, arch: data.payload?.arch }))}</div>
|
||||
<div>${escapeHtml(c2t('c2.payloads.buildSize', { bytes: data.payload?.size_bytes }))}</div>
|
||||
<button onclick="window.__c2DownloadPayload('${data.payload?.download_path?.split('/').pop()}')"
|
||||
<button type="button" data-c2-action="payload-download" data-c2-id="${escapeAttr(data.payload?.download_path?.split('/').pop() || '')}"
|
||||
class="btn-primary" style="margin-top:8px;display:inline-block;cursor:pointer;">${escapeHtml(c2t('c2.payloads.download'))}</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -4213,7 +4290,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="c2-events-table-col-check">
|
||||
<label class="c2-event-check-label" title="${escapeHtml(c2t('c2.events.selectAll'))}">
|
||||
<label class="c2-event-check-label" title="${escapeAttr(c2t('c2.events.selectAll'))}">
|
||||
<input type="checkbox" id="c2-events-select-all" onchange="C2.onEventsSelectAll(this.checked)">
|
||||
</label>
|
||||
</th>
|
||||
@@ -4227,8 +4304,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${C2.events.map(e => {
|
||||
const eid = escapeHtml(e.id || '');
|
||||
${C2.events.map(e => {
|
||||
const rawId = e.id || '';
|
||||
const eid = escapeHtml(rawId);
|
||||
const eidAttr = escapeAttr(rawId);
|
||||
const levelCls = eventLevelBadgeClass(e.level);
|
||||
const catCls = eventCategoryBadgeClass(e.category);
|
||||
const sessionShort = e.sessionId ? escapeHtml(String(e.sessionId).substring(0, 10)) + (String(e.sessionId).length > 10 ? '\u2026' : '') : '';
|
||||
@@ -4236,20 +4315,20 @@
|
||||
const msg = escapeHtml(e.message || '');
|
||||
const rowLevel = escapeHtml(e.level || 'info');
|
||||
return `
|
||||
<tr class="c2-events-row c2-events-row--${rowLevel}" data-event-id="${eid}" onclick="C2.viewEvent('${eid}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();C2.viewEvent('${eid}')}" role="button" tabindex="0">
|
||||
<td class="c2-events-table-col-check" onclick="event.stopPropagation();">
|
||||
<tr class="c2-events-row c2-events-row--${rowLevel}" data-event-id="${eidAttr}" data-c2-action="event-view" data-c2-id="${eidAttr}" role="button" tabindex="0">
|
||||
<td class="c2-events-table-col-check" data-c2-stop-action="1">
|
||||
<label class="c2-event-check-label">
|
||||
<input type="checkbox" class="c2-event-check" data-id="${eid}" onchange="C2.syncEventsToolbar()">
|
||||
<input type="checkbox" class="c2-event-check" data-id="${eidAttr}" onchange="C2.syncEventsToolbar()">
|
||||
</label>
|
||||
</td>
|
||||
<td class="c2-events-col-time">${escapeHtml(formatTime(e.createdAt))}</td>
|
||||
<td><span class="c2-event-level-badge ${levelCls}">${escapeHtml(eventLevelLabel(e.level))}</span></td>
|
||||
<td><span class="${catCls}">${escapeHtml(eventCategoryLabel(e.category))}</span></td>
|
||||
<td class="c2-events-col-message" title="${msg}">${msg || dash}</td>
|
||||
<td class="c2-events-col-mono" title="${escapeHtml(e.sessionId || '')}">${sessionShort || dash}</td>
|
||||
<td class="c2-events-col-mono" title="${escapeHtml(e.taskId || '')}">${taskShort || dash}</td>
|
||||
<td class="c2-events-table-col-actions" onclick="event.stopPropagation();">
|
||||
<button type="button" class="c2-events-delete-btn" data-require-permission="c2:delete" onclick="C2.deleteEventById('${eid}')" title="${delTitle}" aria-label="${delTitle}">${deleteIcon}</button>
|
||||
<td class="c2-events-col-message" title="${escapeAttr(e.message || '')}">${msg || dash}</td>
|
||||
<td class="c2-events-col-mono" title="${escapeAttr(e.sessionId || '')}">${sessionShort || dash}</td>
|
||||
<td class="c2-events-col-mono" title="${escapeAttr(e.taskId || '')}">${taskShort || dash}</td>
|
||||
<td class="c2-events-table-col-actions" data-c2-stop-action="1">
|
||||
<button type="button" class="c2-events-delete-btn" data-require-permission="c2:delete" data-c2-action="event-delete" data-c2-id="${eidAttr}" title="${delTitle}" aria-label="${delTitle}">${deleteIcon}</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
@@ -4364,7 +4443,7 @@
|
||||
<div class="c2-profile-card">
|
||||
<div class="c2-profile-header">
|
||||
<h4>${escapeHtml(p.name)}</h4>
|
||||
<button class="btn-danger btn-sm" data-require-permission="c2:delete" onclick="C2.deleteProfile('${p.id}')">${escapeHtml(c2t('common.delete'))}</button>
|
||||
<button class="btn-danger btn-sm" data-require-permission="c2:delete" data-c2-action="profile-delete" data-c2-id="${escapeAttr(p.id)}">${escapeHtml(c2t('common.delete'))}</button>
|
||||
</div>
|
||||
<div class="c2-profile-info">
|
||||
<div><strong>UA:</strong> ${escapeHtml(p.userAgent || defVal)}</div>
|
||||
@@ -4389,7 +4468,7 @@
|
||||
<div class="c2-modal-body">
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.profiles.profileNameLabel'))}</label>
|
||||
<input type="text" id="c2-profile-name" class="form-control" placeholder="${escapeHtml(c2t('c2.profiles.placeholderProfileName'))}">
|
||||
<input type="text" id="c2-profile-name" class="form-control" placeholder="${escapeAttr(c2t('c2.profiles.placeholderProfileName'))}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.profiles.userAgent'))}</label>
|
||||
|
||||
@@ -13,6 +13,10 @@ let chatFilesPage = 1;
|
||||
let chatFilesPageSize = 20;
|
||||
let chatFilesSearchDebounceTimer = null;
|
||||
|
||||
function chatFilesEscapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
const CHAT_FILES_GROUP_STORAGE_KEY = 'csai_chat_files_group_by';
|
||||
const CHAT_FILES_BROWSE_PATH_KEY = 'csai_chat_files_browse_path';
|
||||
const CHAT_FILES_PAGE_SIZE_STORAGE_KEY = 'csai_chat_files_page_size';
|
||||
@@ -1077,7 +1081,7 @@ function renderChatFilesTable() {
|
||||
<td>${escapeHtml(f.date || '—')}</td>
|
||||
<td class="chat-files-cell-conv"><code title="${convTitleEsc}">${convEsc}</code></td>
|
||||
<td class="chat-files-cell-subpath" title="${escapeHtml(subRaw || '')}">${subCellInner}</td>
|
||||
<td class="chat-files-cell-name" title="${escapeHtml(pathForTitle)}">${nameEsc}${sourceBadge}</td>
|
||||
<td class="chat-files-cell-name" title="${chatFilesEscapeAttr(pathForTitle)}">${nameEsc}${sourceBadge}</td>
|
||||
<td>${formatChatFileBytes(f.size || 0)}</td>
|
||||
<td>${escapeHtml(dt)}</td>
|
||||
<td class="chat-files-actions">
|
||||
@@ -1178,7 +1182,7 @@ function renderChatFilesTable() {
|
||||
? `<button type="button" class="btn-icon btn-danger" title="${tDeleteFolder}" data-chat-folder-name="${nameAttr}" onclick="chatFilesDeleteFolderFromBtn(event, this)">${svgTrash}</button>`
|
||||
: '';
|
||||
return `<tr class="chat-files-tr-folder chat-files-tr-folder--nav" role="button" tabindex="0" data-chat-folder-name="${nameAttr}" onclick="chatFilesOnFolderRowClick(event)" onkeydown="chatFilesOnFolderRowKeydown(event)">
|
||||
<td class="chat-files-tree-name-cell chat-files-tree-name-cell--folder" title="${escapeHtml(folderTitle)}">
|
||||
<td class="chat-files-tree-name-cell chat-files-tree-name-cell--folder" title="${chatFilesEscapeAttr(folderTitle)}">
|
||||
<span class="chat-files-tree-name-inner">${svgFolder}<span class="chat-files-tree-name-text">${escapeHtml(folderDisplay.text)}</span></span>
|
||||
</td>
|
||||
<td class="chat-files-tree-muted">—</td>
|
||||
@@ -1228,7 +1232,7 @@ function renderChatFilesTable() {
|
||||
const menuHtml = menuParts.join('');
|
||||
|
||||
return `<tr class="chat-files-tr-file">
|
||||
<td class="chat-files-tree-name-cell" title="${escapeHtml(pathForTitle)}">
|
||||
<td class="chat-files-tree-name-cell" title="${chatFilesEscapeAttr(pathForTitle)}">
|
||||
<span class="chat-files-tree-name-inner">${svgFile}<span class="chat-files-tree-name-text">${nameEsc}${sourceBadge}</span></span>
|
||||
</td>
|
||||
<td>${formatChatFileBytes(f.size || 0)}</td>
|
||||
|
||||
+25
-7
@@ -8495,7 +8495,14 @@ function createConversationListItemWithMenu(conversation, isPinned) {
|
||||
if (group) {
|
||||
const groupTag = document.createElement('div');
|
||||
groupTag.className = 'conversation-group-tag';
|
||||
groupTag.innerHTML = `<span class="group-tag-icon">${group.icon || '📁'}</span><span class="group-tag-name">${group.name}</span>`;
|
||||
const groupTagIcon = document.createElement('span');
|
||||
groupTagIcon.className = 'group-tag-icon';
|
||||
groupTagIcon.textContent = group.icon || '📁';
|
||||
const groupTagName = document.createElement('span');
|
||||
groupTagName.className = 'group-tag-name';
|
||||
groupTagName.textContent = group.name;
|
||||
groupTag.appendChild(groupTagIcon);
|
||||
groupTag.appendChild(groupTagName);
|
||||
groupTag.title = `分组: ${group.name}`;
|
||||
contentWrapper.appendChild(groupTag);
|
||||
}
|
||||
@@ -9044,12 +9051,23 @@ async function showMoveToGroupSubmenu() {
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'context-submenu-item';
|
||||
item.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>${group.name}</span>
|
||||
`;
|
||||
const folderIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
folderIcon.setAttribute('width', '16');
|
||||
folderIcon.setAttribute('height', '16');
|
||||
folderIcon.setAttribute('viewBox', '0 0 24 24');
|
||||
folderIcon.setAttribute('fill', 'none');
|
||||
folderIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
const folderPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
folderPath.setAttribute('d', 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z');
|
||||
folderPath.setAttribute('stroke', 'currentColor');
|
||||
folderPath.setAttribute('stroke-width', '2');
|
||||
folderPath.setAttribute('stroke-linecap', 'round');
|
||||
folderPath.setAttribute('stroke-linejoin', 'round');
|
||||
folderIcon.appendChild(folderPath);
|
||||
const label = document.createElement('span');
|
||||
label.textContent = group.name;
|
||||
item.appendChild(folderIcon);
|
||||
item.appendChild(label);
|
||||
item.onclick = () => {
|
||||
moveConversationToGroup(contextMenuConversationId, group.id);
|
||||
};
|
||||
|
||||
@@ -166,6 +166,10 @@ if (typeof escapeHtml === 'undefined') {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getFofaFormElements() {
|
||||
return {
|
||||
query: document.getElementById('fofa-query'),
|
||||
@@ -1115,11 +1119,11 @@ function renderFofaResults(payload) {
|
||||
if (f === 'host') {
|
||||
const href = normalizeHttpLink(text);
|
||||
if (href) {
|
||||
const safeHref = escapeHtml(href);
|
||||
return `<td class="info-collect-cell" data-field="${escapeHtml(f)}" data-full="${escapeHtml(text)}" title="${escapeHtml(text)}"><a class="info-collect-link" href="${safeHref}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation();">${escapeHtml(text)}</a></td>`;
|
||||
const safeHref = escapeAttr(href);
|
||||
return `<td class="info-collect-cell" data-field="${escapeAttr(f)}" data-full="${escapeAttr(text)}" title="${escapeAttr(text)}"><a class="info-collect-link" href="${safeHref}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation();">${escapeHtml(text)}</a></td>`;
|
||||
}
|
||||
}
|
||||
return `<td class="info-collect-cell" data-field="${escapeHtml(f)}" data-full="${escapeHtml(text)}" title="${escapeHtml(text)}"><span class="info-collect-cell-text">${escapeHtml(text)}</span></td>`;
|
||||
return `<td class="info-collect-cell" data-field="${escapeAttr(f)}" data-full="${escapeAttr(text)}" title="${escapeAttr(text)}"><span class="info-collect-cell-text">${escapeHtml(text)}</span></td>`;
|
||||
}).join('');
|
||||
|
||||
const actionHtml = `
|
||||
|
||||
+21
-10
@@ -194,7 +194,7 @@ function renderKnowledgeItemsByCategories(categoriesWithItems) {
|
||||
const categoryCount = categoryData.itemCount || categoryItems.length;
|
||||
|
||||
html += `
|
||||
<div class="knowledge-category-section" data-category="${escapeHtml(category)}">
|
||||
<div class="knowledge-category-section" data-category="${escapeAttr(category)}">
|
||||
<div class="knowledge-category-header">
|
||||
<div class="knowledge-category-info">
|
||||
<h3 class="knowledge-category-title">${escapeHtml(category)}</h3>
|
||||
@@ -245,7 +245,7 @@ function renderKnowledgeItems(items) {
|
||||
const categoryCount = categoryItems.length;
|
||||
|
||||
html += `
|
||||
<div class="knowledge-category-section" data-category="${escapeHtml(category)}">
|
||||
<div class="knowledge-category-section" data-category="${escapeAttr(category)}">
|
||||
<div class="knowledge-category-header">
|
||||
<div class="knowledge-category-info">
|
||||
<h3 class="knowledge-category-title">${escapeHtml(category)}</h3>
|
||||
@@ -347,10 +347,10 @@ function renderKnowledgeItemCard(item) {
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="knowledge-item-card" data-id="${item.id}" data-category="${escapeHtml(item.category)}">
|
||||
<div class="knowledge-item-card" data-id="${escapeAttr(item.id)}" data-category="${escapeAttr(item.category)}">
|
||||
<div class="knowledge-item-card-header">
|
||||
<div class="knowledge-item-card-title-row">
|
||||
<h4 class="knowledge-item-card-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</h4>
|
||||
<h4 class="knowledge-item-card-title" title="${escapeAttr(item.title)}">${escapeHtml(item.title)}</h4>
|
||||
<div class="knowledge-item-card-actions">
|
||||
<button class="knowledge-item-action-btn" data-require-permission="knowledge:write" onclick="editKnowledgeItem('${item.id}')" title="编辑">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -1435,13 +1435,13 @@ function renderRetrievalLogs(logs) {
|
||||
${log.conversationId ? `
|
||||
<div class="retrieval-log-detail-item">
|
||||
<span class="detail-label">${_t('retrievalLogs.conversationId')}</span>
|
||||
<code class="detail-value" title="${_t('retrievalLogs.clickToCopy')}" data-copy-title-copied="${_t('common.copied')}" data-copy-title-click="${_t('retrievalLogs.clickToCopy')}" onclick="var t=this; navigator.clipboard.writeText('${escapeHtml(log.conversationId)}').then(function(){ t.title=t.getAttribute('data-copy-title-copied')||'Copied!'; setTimeout(function(){ t.title=t.getAttribute('data-copy-title-click')||'Click to copy'; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.conversationId)}</code>
|
||||
<code class="detail-value" title="${escapeAttr(_t('retrievalLogs.clickToCopy'))}" data-copy-title-copied="${escapeAttr(_t('common.copied'))}" data-copy-title-click="${escapeAttr(_t('retrievalLogs.clickToCopy'))}" onclick="var t=this; navigator.clipboard.writeText(${escapeJsStringAttr(log.conversationId)}).then(function(){ t.title=t.getAttribute('data-copy-title-copied')||'Copied!'; setTimeout(function(){ t.title=t.getAttribute('data-copy-title-click')||'Click to copy'; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.conversationId)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
${log.messageId ? `
|
||||
<div class="retrieval-log-detail-item">
|
||||
<span class="detail-label">${_t('retrievalLogs.messageId')}</span>
|
||||
<code class="detail-value" title="${_t('retrievalLogs.clickToCopy')}" data-copy-title-copied="${_t('common.copied')}" data-copy-title-click="${_t('retrievalLogs.clickToCopy')}" onclick="var el=this; navigator.clipboard.writeText('${escapeHtml(log.messageId)}').then(function(){ el.title=el.getAttribute('data-copy-title-copied')||el.title; setTimeout(function(){ el.title=el.getAttribute('data-copy-title-click')||el.title; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.messageId)}</code>
|
||||
<code class="detail-value" title="${escapeAttr(_t('retrievalLogs.clickToCopy'))}" data-copy-title-copied="${escapeAttr(_t('common.copied'))}" data-copy-title-click="${escapeAttr(_t('retrievalLogs.clickToCopy'))}" onclick="var el=this; navigator.clipboard.writeText(${escapeJsStringAttr(log.messageId)}).then(function(){ el.title=el.getAttribute('data-copy-title-copied')||el.title; setTimeout(function(){ el.title=el.getAttribute('data-copy-title-click')||el.title; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.messageId)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="retrieval-log-detail-item">
|
||||
@@ -1470,7 +1470,7 @@ function renderRetrievalLogs(logs) {
|
||||
</svg>
|
||||
${_t('retrievalLogs.viewDetails')}
|
||||
</button>
|
||||
<button class="btn-secondary btn-sm retrieval-log-delete-btn" onclick="deleteRetrievalLog('${escapeHtml(log.id)}', ${index})" style="margin-top: 12px; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: var(--error-color, #dc3545); border-color: var(--error-color, #dc3545);" onmouseover="this.style.backgroundColor='rgba(220, 53, 69, 0.1)'; this.style.color='#dc3545';" onmouseout="this.style.backgroundColor=''; this.style.color='var(--error-color, #dc3545)';" title="${_t('common.delete')}">
|
||||
<button class="btn-secondary btn-sm retrieval-log-delete-btn" onclick="deleteRetrievalLog(${escapeJsStringAttr(log.id)}, ${index})" style="margin-top: 12px; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: var(--error-color, #dc3545); border-color: var(--error-color, #dc3545);" onmouseover="this.style.backgroundColor='rgba(220, 53, 69, 0.1)'; this.style.color='#dc3545';" onmouseout="this.style.backgroundColor=''; this.style.color='var(--error-color, #dc3545)';" title="${escapeAttr(_t('common.delete'))}">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M19 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" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -1910,7 +1910,7 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.conversationId')}</div>
|
||||
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
|
||||
onclick="navigator.clipboard.writeText('${escapeHtml(log.conversationId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
|
||||
onclick="navigator.clipboard.writeText(${escapeJsStringAttr(log.conversationId)}); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
|
||||
title="点击复制">${escapeHtml(log.conversationId)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -1918,7 +1918,7 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.messageId')}</div>
|
||||
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
|
||||
onclick="navigator.clipboard.writeText('${escapeHtml(log.messageId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
|
||||
onclick="navigator.clipboard.writeText(${escapeJsStringAttr(log.messageId)}); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
|
||||
title="点击复制">${escapeHtml(log.messageId)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -2006,6 +2006,18 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttr(escapeJsString(text));
|
||||
}
|
||||
|
||||
function formatTime(timeStr) {
|
||||
if (!timeStr) return '';
|
||||
|
||||
@@ -2319,4 +2331,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+35
-23
@@ -564,6 +564,18 @@ function escapeHtmlLocal(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeAttrLocal(text) {
|
||||
return escapeHtmlLocal(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttrLocal(escapeJsString(text));
|
||||
}
|
||||
|
||||
function formatTimelinePlainTextHtml(text) {
|
||||
return '<pre class="timeline-plain-text">' + escapeHtml(text == null ? '' : String(text)) + '</pre>';
|
||||
}
|
||||
@@ -5786,7 +5798,7 @@ function buildMcpTimelineSvg(points, rangeKey) {
|
||||
const isPeak = c.i === peakIdx && (c.p.total || 0) > 0;
|
||||
const dotClass = 'mcp-stats-timeline-dot' + (isPeak ? ' mcp-stats-timeline-dot--peak' : '');
|
||||
return `<circle class="${dotClass}" cx="${c.x.toFixed(2)}" cy="${c.y.toFixed(2)}" r="${isPeak ? 2 : 1.5}"
|
||||
data-time="${escapeHtml(tipTime)}"
|
||||
data-time="${escapeAttrLocal(tipTime)}"
|
||||
data-total="${c.p.total || 0}"
|
||||
data-failed="${c.p.failed || 0}" />`;
|
||||
}).join('');
|
||||
@@ -5800,9 +5812,9 @@ function buildMcpTimelineSvg(points, rangeKey) {
|
||||
const tipTime = formatMcpTimelineLabel(c.p.t, rangeKey, locale);
|
||||
return `<g class="mcp-stats-timeline-bar-group">
|
||||
<rect class="mcp-stats-timeline-bar${total > 0 ? ' is-active' : ''}" x="${(c.x - barW / 2).toFixed(2)}" y="${y.toFixed(2)}" width="${barW.toFixed(2)}" height="${h.toFixed(2)}" rx="1.6"
|
||||
data-time="${escapeHtml(tipTime)}" data-total="${total}" data-failed="${failed}" />
|
||||
data-time="${escapeAttrLocal(tipTime)}" data-total="${total}" data-failed="${failed}" />
|
||||
${failedH > 0 ? `<rect class="mcp-stats-timeline-bar-fail" x="${(c.x - barW / 2).toFixed(2)}" y="${(baseY - failedH).toFixed(2)}" width="${barW.toFixed(2)}" height="${failedH.toFixed(2)}" rx="1.6"
|
||||
data-time="${escapeHtml(tipTime)}" data-total="${total}" data-failed="${failed}" />` : ''}
|
||||
data-time="${escapeAttrLocal(tipTime)}" data-total="${total}" data-failed="${failed}" />` : ''}
|
||||
</g>`;
|
||||
}).join('');
|
||||
|
||||
@@ -6421,11 +6433,11 @@ function renderMcpStatsInsightPanel(topTools, totals, activeToolFilter = '', opt
|
||||
|| `${s.name},${s.calls} 次调用,占 ${s.pct}%`;
|
||||
return `<li class="mcp-stats-dist-legend-item-wrap">
|
||||
<button type="button" class="mcp-stats-dist-legend-item${isActive ? ' is-active' : ''}"
|
||||
data-tool-name="${escapeHtml(s.name)}"
|
||||
data-tool-name="${escapeAttrLocal(s.name)}"
|
||||
data-pct="${s.pct}"
|
||||
data-calls="${s.calls}"
|
||||
data-is-others="0"
|
||||
aria-label="${escapeHtml(rowAria)}"
|
||||
aria-label="${escapeAttrLocal(rowAria)}"
|
||||
aria-pressed="${isActive ? 'true' : 'false'}">${inner}</button>
|
||||
</li>`;
|
||||
}).join('');
|
||||
@@ -6452,8 +6464,8 @@ function renderMcpStatsInsightPanel(topTools, totals, activeToolFilter = '', opt
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<div class="mcp-stats-dist-panel${embedded ? ' mcp-stats-dist-panel--embedded' : ''}" aria-label="${escapeHtml(distTitle)}"
|
||||
data-center-label="${escapeHtml(centerLabel)}"
|
||||
<div class="mcp-stats-dist-panel${embedded ? ' mcp-stats-dist-panel--embedded' : ''}" aria-label="${escapeAttrLocal(distTitle)}"
|
||||
data-center-label="${escapeAttrLocal(centerLabel)}"
|
||||
data-center-value="${top6SharePct}"
|
||||
data-center-suffix="%">
|
||||
${headerHtml}
|
||||
@@ -6664,13 +6676,13 @@ function renderMcpStatsToolTable(topTools, totals, activeToolFilter = '') {
|
||||
|| `${name},${total} 次调用,成功率 ${toolRate}%`;
|
||||
rowsHtml += `
|
||||
<tr class="mcp-stats-tool-row${isActive ? ' is-active' : ''}"
|
||||
data-tool-name="${escapeHtml(rawName)}"
|
||||
data-tool-name="${escapeAttrLocal(rawName)}"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="${escapeHtml(rowAria)}"
|
||||
aria-label="${escapeAttrLocal(rowAria)}"
|
||||
aria-pressed="${isActive ? 'true' : 'false'}">
|
||||
<td class="col-rank"><span class="mcp-stats-rank${rankClass}">${index + 1}</span></td>
|
||||
<td class="col-tool" title="${escapeHtml(name)}">
|
||||
<td class="col-tool" title="${escapeAttrLocal(name)}">
|
||||
<span class="mcp-stats-tool-dot" style="background:${dotColor}" aria-hidden="true"></span>
|
||||
<span class="mcp-stats-tool-label">${escapeHtml(name)}</span>
|
||||
</td>
|
||||
@@ -6719,8 +6731,8 @@ function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') {
|
||||
const segAria = mcpMonitorT('distSegmentAria', { name: displayName, pct: s.pct, calls: s.calls })
|
||||
|| `${displayName},占 ${s.pct}%,${s.calls} 次`;
|
||||
return `<span class="mcp-stats-proportion-seg${isActive ? ' is-active' : ''}"
|
||||
data-tool-name="${escapeHtml(s.name)}" data-pct="${s.pct}" data-calls="${s.calls}" data-is-others="0"
|
||||
role="button" tabindex="0" aria-label="${escapeHtml(segAria)}"
|
||||
data-tool-name="${escapeAttrLocal(s.name)}" data-pct="${s.pct}" data-calls="${s.calls}" data-is-others="0"
|
||||
role="button" tabindex="0" aria-label="${escapeAttrLocal(segAria)}"
|
||||
style="flex:${s.pctNum} 1 0;background:${s.color}" title="${escapeHtml(title)}"></span>`;
|
||||
}).join('');
|
||||
|
||||
@@ -6748,12 +6760,12 @@ function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') {
|
||||
const successLabel = mcpMonitorT('successCount', { n: success }) || `成功 ${success}`;
|
||||
const failedLabel = mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`;
|
||||
return `<li class="mcp-stats-tool-item${isActive ? ' is-active' : ''}"
|
||||
data-tool-name="${escapeHtml(rawName)}" tabindex="0" role="button"
|
||||
aria-label="${escapeHtml(rowAria)}" aria-pressed="${isActive ? 'true' : 'false'}">
|
||||
data-tool-name="${escapeAttrLocal(rawName)}" tabindex="0" role="button"
|
||||
aria-label="${escapeAttrLocal(rowAria)}" aria-pressed="${isActive ? 'true' : 'false'}">
|
||||
<div class="mcp-stats-tool-item__top">
|
||||
<span class="mcp-stats-tool-item__rank mcp-stats-rank${rankClass}">${index + 1}</span>
|
||||
<span class="mcp-stats-tool-item__dot" style="background:${color}" aria-hidden="true"></span>
|
||||
<span class="mcp-stats-tool-item__name" title="${escapeHtml(name)}">${escapeHtml(name)}</span>
|
||||
<span class="mcp-stats-tool-item__name" title="${escapeAttrLocal(name)}">${escapeHtml(name)}</span>
|
||||
<span class="mcp-stats-tool-item__share">${sharePct}%</span>
|
||||
</div>
|
||||
<div class="mcp-stats-tool-item__middle">
|
||||
@@ -6813,8 +6825,8 @@ function renderMcpStatsChartAside(topTools, totals, activeToolFilter = '') {
|
||||
|
||||
return `
|
||||
<div class="mcp-stats-dist-panel mcp-stats-dist-panel--compact"
|
||||
aria-label="${escapeHtml(distTitle)}"
|
||||
data-center-label="${escapeHtml(centerLabel)}"
|
||||
aria-label="${escapeAttrLocal(distTitle)}"
|
||||
data-center-label="${escapeAttrLocal(centerLabel)}"
|
||||
data-center-value="${top6SharePct}"
|
||||
data-center-suffix="%">
|
||||
<p class="mcp-stats-panel__aside-title">${escapeHtml(distTitle)}</p>
|
||||
@@ -6975,11 +6987,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
const duration = formatExecutionDuration(exec.startTime, exec.endTime);
|
||||
const toolName = escapeHtml(formatMonitorToolName(exec.toolName) || unknownToolLabel);
|
||||
const rawExecId = exec.id || '';
|
||||
const executionId = escapeHtml(rawExecId);
|
||||
const executionId = escapeAttrLocal(rawExecId);
|
||||
const jsExecId = escapeJsStringAttr(rawExecId);
|
||||
const terminateBtn = status === 'running'
|
||||
? `<button type="button" class="btn-secondary btn-monitor-abort" data-require-permission="monitor:write" onclick="cancelMCPToolExecution('${rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')">${escapeHtml(terminateLabel)}</button>`
|
||||
? `<button type="button" class="btn-secondary btn-monitor-abort" data-require-permission="monitor:write" onclick="cancelMCPToolExecution(${jsExecId})">${escapeHtml(terminateLabel)}</button>`
|
||||
: '';
|
||||
const jsExecId = rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
const isSelected = monitorState.selectedExecutions.has(rawExecId);
|
||||
const rowKey = monitorRenderKey([exec, isSelected, locale || 'en-US']);
|
||||
return {
|
||||
@@ -6988,7 +7000,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
html: `
|
||||
<tr data-execution-id="${executionId}">
|
||||
<td>
|
||||
<input type="checkbox" class="monitor-execution-checkbox theme-checkbox" value="${executionId}" ${isSelected ? 'checked' : ''} onchange="toggleExecutionSelection('${jsExecId}', this.checked)" />
|
||||
<input type="checkbox" class="monitor-execution-checkbox theme-checkbox" value="${executionId}" ${isSelected ? 'checked' : ''} onchange="toggleExecutionSelection(${jsExecId}, this.checked)" />
|
||||
</td>
|
||||
<td>${toolName}</td>
|
||||
<td><span class="${statusClass}">${escapeHtml(statusLabel)}</span></td>
|
||||
@@ -6996,9 +7008,9 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
<td class="monitor-execution-duration">${escapeHtml(duration)}</td>
|
||||
<td>
|
||||
<div class="monitor-execution-actions">
|
||||
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">${escapeHtml(viewDetailLabel)}</button>
|
||||
<button class="btn-secondary" onclick="showMCPDetail(${jsExecId})">${escapeHtml(viewDetailLabel)}</button>
|
||||
${terminateBtn}
|
||||
<button class="btn-secondary btn-delete" data-require-permission="monitor:delete" onclick="deleteExecution('${executionId}')" title="${escapeHtml(deleteExecTitle)}">${escapeHtml(deleteLabel)}</button>
|
||||
<button class="btn-secondary btn-delete" data-require-permission="monitor:delete" onclick="deleteExecution(${jsExecId})" title="${escapeHtml(deleteExecTitle)}">${escapeHtml(deleteLabel)}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -897,12 +897,12 @@ function renderProjectsSidebar() {
|
||||
p.pinned ? `<span class="projects-list-item-badge">${escapeHtml(tp('projects.pinned'))}</span>` : '',
|
||||
p.status === 'archived' ? `<span class="projects-list-item-badge">${escapeHtml(tp('projects.archived'))}</span>` : '',
|
||||
].join('');
|
||||
return `<div class="projects-list-item${active}${archived}" data-id="${escapeHtml(p.id)}" onclick="selectProject('${escapeHtml(p.id)}')">
|
||||
return `<div class="projects-list-item${active}${archived}" data-id="${escapeAttr(p.id)}" onclick="selectProject(${escapeJsStringAttr(p.id)})">
|
||||
<div class="projects-list-item-body">
|
||||
<div class="projects-list-item-name">${escapeHtml(p.name)}${badges}</div>
|
||||
<div class="projects-list-item-meta">${formatProjectTime(p.updated_at)}</div>
|
||||
</div>
|
||||
<button type="button" class="projects-list-item-menu" title="${escapeHtml(tp('projects.projectActions'))}" aria-label="${escapeHtml(tp('projects.projectActions'))}" onclick="showProjectListActionMenu(event, '${escapeHtml(p.id)}')">⋯</button>
|
||||
<button type="button" class="projects-list-item-menu" title="${escapeHtml(tp('projects.projectActions'))}" aria-label="${escapeHtml(tp('projects.projectActions'))}" onclick="showProjectListActionMenu(event, ${escapeJsStringAttr(p.id)})">⋯</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
updateProjectsDetailVisibility();
|
||||
@@ -1324,8 +1324,8 @@ function renderGraphEdgesListHtml(factKey, graphData, selectedEdgeId) {
|
||||
const synthetic = isSyntheticGraphEdge(e);
|
||||
const deleteBtn = synthetic
|
||||
? `<span class="project-fact-graph-edge-synthetic" title="${escapeHtml(tp('projects.graphEdgeSynthetic'))}">—</span>`
|
||||
: `<button type="button" class="project-fact-graph-edge-delete" data-edge-id="${escapeHtml(e.id)}" onclick="event.stopPropagation(); deleteProjectFactEdge(this.dataset.edgeId)" title="${escapeHtml(tp('projects.graphDeleteEdge'))}"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>`;
|
||||
return `<div class="project-fact-graph-edge-item${selected}" data-edge-id="${escapeHtml(e.id)}" onclick="focusProjectFactGraphEdge(${JSON.stringify(e.id)})">
|
||||
: `<button type="button" class="project-fact-graph-edge-delete" data-edge-id="${escapeAttr(e.id)}" onclick="event.stopPropagation(); deleteProjectFactEdge(this.dataset.edgeId)" title="${escapeAttr(tp('projects.graphDeleteEdge'))}"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>`;
|
||||
return `<div class="project-fact-graph-edge-item${selected}" data-edge-id="${escapeAttr(e.id)}" onclick="focusProjectFactGraphEdge(${escapeJsStringAttr(e.id)})">
|
||||
<span class="project-fact-graph-edge-dir">${escapeHtml(dirLabel)}</span>
|
||||
<span class="project-fact-graph-edge-type">${escapeHtml(e.type || '')}</span>
|
||||
<span class="project-fact-graph-edge-peer" title="${escapeHtml(src + ' → ' + tgt)}">${escapeHtml(src)} → ${escapeHtml(tgt)}</span>
|
||||
@@ -2461,6 +2461,18 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function escapeAttr(s) {
|
||||
return escapeHtml(s).replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttr(escapeJsString(text));
|
||||
}
|
||||
|
||||
function getChatProjectSelection() {
|
||||
const convId = window.currentConversationId;
|
||||
if (convId) {
|
||||
|
||||
+22
-10
@@ -552,6 +552,18 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttr(escapeJsString(text));
|
||||
}
|
||||
|
||||
// 刷新角色列表
|
||||
async function refreshRoles() {
|
||||
await loadRoles();
|
||||
@@ -651,8 +663,8 @@ function renderRolesList() {
|
||||
<span class="role-card-tools-value">${toolsDisplay}</span>
|
||||
</div>
|
||||
<div class="role-card-actions">
|
||||
<button class="btn-secondary btn-small" onclick="editRole('${escapeHtml(role.name)}')">${_t('common.edit')}</button>
|
||||
${role.name !== '默认' ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteRole('${escapeHtml(role.name)}')">${_t('common.delete')}</button>` : ''}
|
||||
<button class="btn-secondary btn-small" onclick="editRole(${escapeJsStringAttr(role.name)})">${_t('common.edit')}</button>
|
||||
${role.name !== '默认' ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteRole(${escapeJsStringAttr(role.name)})">${_t('common.delete')}</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -924,7 +936,7 @@ function renderRoleToolsList() {
|
||||
return;
|
||||
}
|
||||
|
||||
const chkTitle = escapeHtml(_t('roleModal.checkboxLinkTitle'));
|
||||
const chkTitle = escapeAttr(_t('roleModal.checkboxLinkTitle'));
|
||||
|
||||
allRoleTools.forEach(tool => {
|
||||
const toolKey = getToolKey(tool);
|
||||
@@ -948,19 +960,19 @@ function renderRoleToolsList() {
|
||||
const externalMcpName = toolState.external_mcp || tool.external_mcp || '';
|
||||
const badgeText = externalMcpName ? `外部 (${escapeHtml(externalMcpName)})` : '外部';
|
||||
const badgeTitle = externalMcpName ? `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}` : '外部MCP工具';
|
||||
externalBadge = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
|
||||
externalBadge = `<span class="external-tool-badge" title="${escapeAttr(badgeTitle)}">${badgeText}</span>`;
|
||||
}
|
||||
let mcpDisabledBadge = '';
|
||||
if (tool.enabled === false) {
|
||||
mcpDisabledBadge = `<span class="role-tool-mcp-disabled-badge" title="${escapeHtml(_t('roleModal.mcpDisabledBadgeTitle'))}">${escapeHtml(_t('roleModal.mcpDisabledBadge'))}</span>`;
|
||||
}
|
||||
// 生成唯一的checkbox id
|
||||
const checkboxId = `role-tool-${escapeHtml(toolKey).replace(/::/g, '--')}`;
|
||||
const checkboxId = `role-tool-${escapeAttr(toolKey).replace(/::/g, '--')}`;
|
||||
|
||||
toolItem.innerHTML = `
|
||||
<input type="checkbox" id="${checkboxId}" ${toolState.enabled ? 'checked' : ''}
|
||||
title="${chkTitle}" aria-label="${chkTitle}"
|
||||
onchange="handleRoleToolCheckboxChange('${escapeHtml(toolKey)}', this.checked)" />
|
||||
onchange="handleRoleToolCheckboxChange(${escapeJsStringAttr(toolKey)}, this.checked)" />
|
||||
<div class="role-tool-item-info">
|
||||
<div class="role-tool-item-name">
|
||||
${escapeHtml(tool.name)}
|
||||
@@ -1011,11 +1023,11 @@ function renderRoleToolsPagination() {
|
||||
</select>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="loadRoleTools(1, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 || navDisabled ? 'disabled' : ''}>${_t('roleModal.firstPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadRoleTools(${page - 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 || navDisabled ? 'disabled' : ''}>${_t('roleModal.prevPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadRoleTools(1, ${escapeJsStringAttr(roleToolsSearchKeyword)})" ${page === 1 || navDisabled ? 'disabled' : ''}>${_t('roleModal.firstPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadRoleTools(${page - 1}, ${escapeJsStringAttr(roleToolsSearchKeyword)})" ${page === 1 || navDisabled ? 'disabled' : ''}>${_t('roleModal.prevPage')}</button>
|
||||
<span class="pagination-page">${_t('roleModal.pageOf', { page: page, total: totalPages })}</span>
|
||||
<button class="btn-secondary" onclick="loadRoleTools(${page + 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages || navDisabled ? 'disabled' : ''}>${_t('roleModal.nextPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadRoleTools(${totalPages}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages || navDisabled ? 'disabled' : ''}>${_t('roleModal.lastPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadRoleTools(${page + 1}, ${escapeJsStringAttr(roleToolsSearchKeyword)})" ${page === totalPages || navDisabled ? 'disabled' : ''}>${_t('roleModal.nextPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadRoleTools(${totalPages}, ${escapeJsStringAttr(roleToolsSearchKeyword)})" ${page === totalPages || navDisabled ? 'disabled' : ''}>${_t('roleModal.lastPage')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
+26
-14
@@ -21,6 +21,18 @@ function settingsT(key, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function settingsEscapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function settingsEscapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function settingsEscapeJsStringAttr(text) {
|
||||
return settingsEscapeAttr(settingsEscapeJsString(text));
|
||||
}
|
||||
|
||||
const settingsCustomSelects = new Map();
|
||||
let settingsCustomSelectsDocBound = false;
|
||||
|
||||
@@ -1371,23 +1383,23 @@ function renderToolsList() {
|
||||
const badgeText = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalFrom', { name: escapeHtml(externalMcpName) }) : `外部 (${escapeHtml(externalMcpName)})`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部');
|
||||
const badgeTitle = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalToolFrom', { name: escapeHtml(externalMcpName) }) + ' — 点击跳转' : `外部MCP工具 - 来源:${escapeHtml(externalMcpName)} — 点击跳转`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部MCP工具');
|
||||
if (externalMcpName) {
|
||||
externalBadge = `<span class="external-tool-badge clickable" onclick="scrollToExternalMCP('${escapeHtml(externalMcpName)}', event)" title="${badgeTitle}">${badgeText}</span>`;
|
||||
externalBadge = `<span class="external-tool-badge clickable" onclick="scrollToExternalMCP(${settingsEscapeJsStringAttr(externalMcpName)}, event)" title="${settingsEscapeAttr(badgeTitle)}">${badgeText}</span>`;
|
||||
} else {
|
||||
externalBadge = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
|
||||
externalBadge = `<span class="external-tool-badge" title="${settingsEscapeAttr(badgeTitle)}">${badgeText}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一的checkbox id,使用工具唯一标识符
|
||||
const checkboxId = `tool-${escapeHtml(toolKey).replace(/::/g, '--')}`;
|
||||
const checkboxId = `tool-${settingsEscapeAttr(toolKey).replace(/::/g, '--')}`;
|
||||
|
||||
toolItem.innerHTML = `
|
||||
<input type="checkbox" class="theme-checkbox" id="${checkboxId}" ${toolState.enabled ? 'checked' : ''} ${toolState.is_external || tool.is_external ? 'data-external="true"' : ''} onchange="handleToolCheckboxChange('${escapeHtml(toolKey)}', this.checked)" />
|
||||
<input type="checkbox" class="theme-checkbox" id="${checkboxId}" ${toolState.enabled ? 'checked' : ''} ${toolState.is_external || tool.is_external ? 'data-external="true"' : ''} onchange="handleToolCheckboxChange(${settingsEscapeJsStringAttr(toolKey)}, this.checked)" />
|
||||
<div class="tool-item-info">
|
||||
<div class="tool-item-name">
|
||||
${escapeHtml(tool.name)}
|
||||
${externalBadge}
|
||||
<label class="tool-resident-toggle" title="${typeof window.t === 'function' ? window.t('mcp.alwaysVisibleHint') : '始终常驻在 Tool Search 可见列表'}" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" class="theme-checkbox" ${alwaysVisibleChecked ? 'checked' : ''} ${alwaysVisibleLocked ? 'disabled' : ''} onchange="handleToolAlwaysVisibleChange('${escapeHtml(toolKey)}', this.checked)" />
|
||||
<input type="checkbox" class="theme-checkbox" ${alwaysVisibleChecked ? 'checked' : ''} ${alwaysVisibleLocked ? 'disabled' : ''} onchange="handleToolAlwaysVisibleChange(${settingsEscapeJsStringAttr(toolKey)}, this.checked)" />
|
||||
<span>${typeof window.t === 'function' ? window.t('mcp.alwaysVisibleLabel') : '常驻'}</span>
|
||||
</label>
|
||||
${alwaysVisibleLocked ? `<span class="external-tool-badge" title="${typeof window.t === 'function' ? window.t('mcp.alwaysVisibleBuiltinHint') : '后端内置工具默认常驻,不可关闭'}">${typeof window.t === 'function' ? window.t('mcp.alwaysVisibleBuiltinLabel') : '内置默认'}</span>` : ''}
|
||||
@@ -1644,11 +1656,11 @@ function renderToolsPagination() {
|
||||
</select>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="loadToolsList(1, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${t('mcp.firstPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page - 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${t('mcp.prevPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(1, ${settingsEscapeJsStringAttr(toolsSearchKeyword)})" ${page === 1 ? 'disabled' : ''}>${t('mcp.firstPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page - 1}, ${settingsEscapeJsStringAttr(toolsSearchKeyword)})" ${page === 1 ? 'disabled' : ''}>${t('mcp.prevPage')}</button>
|
||||
<span class="pagination-page">${paginationT('mcp.pageInfo', { page: page, total: totalPages })}</span>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page + 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${t('mcp.nextPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${totalPages}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${t('mcp.lastPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page + 1}, ${settingsEscapeJsStringAttr(toolsSearchKeyword)})" ${page === totalPages ? 'disabled' : ''}>${t('mcp.nextPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${totalPages}, ${settingsEscapeJsStringAttr(toolsSearchKeyword)})" ${page === totalPages ? 'disabled' : ''}>${t('mcp.lastPage')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -4096,7 +4108,7 @@ function renderExternalMCPList(servers) {
|
||||
const selectedClass = toolsExternalMcpFilter === name ? ' selected' : '';
|
||||
|
||||
html += `
|
||||
<div class="${cardClass}${selectedClass}" data-mcp-name="${escapeHtml(name)}"${hasTools ? ` onclick="scrollToExternalMCPTools('${escapeHtml(name)}', event)" title="${cardClickTitle}"` : ''}>
|
||||
<div class="${cardClass}${selectedClass}" data-mcp-name="${settingsEscapeAttr(name)}"${hasTools ? ` onclick="scrollToExternalMCPTools(${settingsEscapeJsStringAttr(name)}, event)" title="${settingsEscapeAttr(cardClickTitle)}"` : ''}>
|
||||
<div class="external-mcp-item-header">
|
||||
<div class="external-mcp-item-info">
|
||||
<h4>${transportIcon} ${escapeHtml(name)}${server.tool_count !== undefined && server.tool_count > 0 ? `<span class="tool-count-badge" title="${escapeHtml(statusT('mcp.toolCount'))}">🔧 ${server.tool_count}</span>` : ''}</h4>
|
||||
@@ -4104,15 +4116,15 @@ function renderExternalMCPList(servers) {
|
||||
</div>
|
||||
<div class="external-mcp-item-actions">
|
||||
${status === 'connected' || status === 'disconnected' || status === 'error' || status === 'disabled' ?
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" onclick="toggleExternalMCP('${escapeHtml(name)}', '${status}')" title="${status === 'connected' ? statusT('mcp.stopConnection') : statusT('mcp.startConnection')}">
|
||||
`<button class="btn-small" id="btn-toggle-${settingsEscapeAttr(name)}" onclick="toggleExternalMCP(${settingsEscapeJsStringAttr(name)}, ${settingsEscapeJsStringAttr(status)})" title="${settingsEscapeAttr(status === 'connected' ? statusT('mcp.stopConnection') : statusT('mcp.startConnection'))}">
|
||||
${status === 'connected' ? '⏸ ' + statusT('mcp.stop') : '▶ ' + statusT('mcp.start')}
|
||||
</button>` :
|
||||
status === 'connecting' ?
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" disabled style="opacity: 0.6; cursor: not-allowed;">
|
||||
`<button class="btn-small" id="btn-toggle-${settingsEscapeAttr(name)}" disabled style="opacity: 0.6; cursor: not-allowed;">
|
||||
⏳ ${statusT('mcp.connecting')}
|
||||
</button>` : ''}
|
||||
<button class="btn-small" onclick="editExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.editConfig')}" ${status === 'connecting' ? 'disabled' : ''}>✏️ ${statusT('common.edit')}</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.deleteConfig')}" ${status === 'connecting' ? 'disabled' : ''}>🗑 ${statusT('common.delete')}</button>
|
||||
<button class="btn-small" onclick="editExternalMCP(${settingsEscapeJsStringAttr(name)})" title="${settingsEscapeAttr(statusT('mcp.editConfig'))}" ${status === 'connecting' ? 'disabled' : ''}>✏️ ${statusT('common.edit')}</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteExternalMCP(${settingsEscapeJsStringAttr(name)})" title="${settingsEscapeAttr(statusT('mcp.deleteConfig'))}" ${status === 'connecting' ? 'disabled' : ''}>🗑 ${statusT('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
${(status === 'error' || status === 'disconnected') && server.error ? `
|
||||
|
||||
@@ -186,9 +186,9 @@ function renderSkillsList() {
|
||||
${tagsHtml}
|
||||
</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>
|
||||
<button type="button" class="btn-secondary btn-small" data-skill-view="${escapeAttr(sid)}">${_t('common.view')}</button>
|
||||
<button type="button" class="btn-secondary btn-small" data-skill-edit="${escapeAttr(sid)}">${_t('common.edit')}</button>
|
||||
<button type="button" class="btn-secondary btn-small btn-danger" data-skill-delete="${escapeAttr(sid)}">${_t('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,7 +526,7 @@ function renderSkillPackageTree() {
|
||||
`</div>`;
|
||||
}
|
||||
const selected = path === skillActivePath ? ' is-selected' : '';
|
||||
return `<div class="skill-tree-row skill-tree-file${selected}" style="padding-left:${indent}px" data-skill-tree-path="${escapeHtml(path)}" title="${escapeHtml(_t('skillModal.clickToEdit'))}">` +
|
||||
return `<div class="skill-tree-row skill-tree-file${selected}" style="padding-left:${indent}px" data-skill-tree-path="${escapeAttr(path)}" title="${escapeAttr(_t('skillModal.clickToEdit'))}">` +
|
||||
`<span class="skill-tree-icon" aria-hidden="true">📄</span>` +
|
||||
`<span class="skill-tree-label">${escapeHtml(path)}</span>` +
|
||||
`</div>`;
|
||||
@@ -1080,6 +1080,10 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 语言切换时重新渲染当前页(技能列表与分页使用 _t,需随语言更新)
|
||||
document.addEventListener('languagechange', function () {
|
||||
const page = document.getElementById('page-skills-management');
|
||||
|
||||
+34
-22
@@ -108,6 +108,18 @@ if (typeof escapeHtml === 'undefined') {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttr(escapeJsString(text));
|
||||
}
|
||||
|
||||
// 任务管理状态
|
||||
const tasksState = {
|
||||
allTasks: [],
|
||||
@@ -526,33 +538,33 @@ function renderTaskItem(task, statusMap, isHistory = false) {
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="task-item ${isHistory ? 'task-item-history' : ''}" data-task-id="${task.conversationId}" data-started-at="${task.startedAt}" data-status="${task.status}">
|
||||
<div class="task-item ${isHistory ? 'task-item-history' : ''}" data-task-id="${escapeAttr(task.conversationId)}" data-started-at="${escapeAttr(task.startedAt)}" data-status="${escapeAttr(task.status)}">
|
||||
<div class="task-header">
|
||||
<div class="task-info">
|
||||
${canCancel ? `
|
||||
<label class="task-checkbox">
|
||||
<input type="checkbox" ${isSelected ? 'checked' : ''}
|
||||
onchange="toggleTaskSelection('${task.conversationId}', this.checked)">
|
||||
onchange="toggleTaskSelection(${escapeJsStringAttr(task.conversationId)}, this.checked)">
|
||||
</label>
|
||||
` : '<div class="task-checkbox-placeholder"></div>'}
|
||||
<span class="task-status ${status.class}">${status.text}</span>
|
||||
${isHistory ? '<span class="task-history-badge" title="' + _t('tasks.historyBadge') + '">📜</span>' : ''}
|
||||
<span class="task-message" title="${escapeHtml((task.title || task.message || _t('tasks.unnamedTask')))}">${escapeHtml((task.title || task.message || _t('tasks.unnamedTask')))}</span>
|
||||
<span class="task-message" title="${escapeAttr((task.title || task.message || _t('tasks.unnamedTask')))}">${escapeHtml((task.title || task.message || _t('tasks.unnamedTask')))}</span>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
${duration ? `<span class="task-duration" title="${_t('tasks.duration')}">⏱ ${duration}</span>` : ''}
|
||||
<span class="task-time" title="${isHistory && completedText ? _t('tasks.completedAt') : _t('tasks.startedAt')}">
|
||||
${isHistory && completedText ? completedText : timeText}
|
||||
</span>
|
||||
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask('${task.conversationId}', this)">` + _t('tasks.cancelTask') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="navigateToVulnerabilitiesFromTasksPage('conversation', '${task.conversationId}')">` + _t('tasks.viewVulnerabilities') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation('${task.conversationId}')">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask(${escapeJsStringAttr(task.conversationId)}, this)">` + _t('tasks.cancelTask') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="navigateToVulnerabilitiesFromTasksPage('conversation', ${escapeJsStringAttr(task.conversationId)})">` + _t('tasks.viewVulnerabilities') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation(${escapeJsStringAttr(task.conversationId)})">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
${task.conversationId ? `
|
||||
<div class="task-details">
|
||||
<span class="task-id-label">` + _t('tasks.conversationIdLabel') + `:</span>
|
||||
<span class="task-id-value" title="` + _t('tasks.clickToCopy') + `" onclick="copyTaskId('${task.conversationId}')">${escapeHtml(task.conversationId)}</span>
|
||||
<span class="task-id-value" title="` + _t('tasks.clickToCopy') + `" onclick="copyTaskId(${escapeJsStringAttr(task.conversationId)})">${escapeHtml(task.conversationId)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -1593,7 +1605,7 @@ function renderBatchQueues() {
|
||||
const doneCount = stats.completed + stats.failed + stats.cancelled;
|
||||
|
||||
return `
|
||||
<div class="batch-queue-item batch-queue-item--compact${cardMod}${noActionsClass}" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
|
||||
<div class="batch-queue-item batch-queue-item--compact${cardMod}${noActionsClass}" data-queue-id="${escapeAttr(queue.id)}" onclick="showBatchQueueDetail(${escapeJsStringAttr(queue.id)})">
|
||||
<div class="batch-queue-item__inner batch-queue-item__inner--grid">
|
||||
<div class="batch-queue-item__lead">
|
||||
<div class="batch-queue-item__title-row">
|
||||
@@ -1601,7 +1613,7 @@ function renderBatchQueues() {
|
||||
<div class="batch-queue-item__titles">${titleBlock}</div>
|
||||
</div>
|
||||
<p class="batch-queue-item__config">${configLine}${cronPausedNote}</p>
|
||||
<p class="batch-queue-item__idline batch-queue-item__idline--lead"><code title="${escapeHtml(queue.id)}">${shortId}</code><span class="batch-queue-item__idsep">\u00b7</span><span>${escapeHtml(_t('tasks.createdTimeLabel'))}\u00a0${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></p>
|
||||
<p class="batch-queue-item__idline batch-queue-item__idline--lead"><code title="${escapeAttr(queue.id)}">${shortId}</code><span class="batch-queue-item__idsep">\u00b7</span><span>${escapeHtml(_t('tasks.createdTimeLabel'))}\u00a0${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></p>
|
||||
</div>
|
||||
<div class="batch-queue-item__cluster">
|
||||
<div class="batch-queue-item__status-inline">
|
||||
@@ -1616,8 +1628,8 @@ function renderBatchQueues() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="batch-queue-item__actions-col" onclick="event.stopPropagation();">
|
||||
<button type="button" class="batch-queue-icon-btn" onclick="navigateToVulnerabilitiesFromTasksPage('queue', '${queue.id}')" title="${escapeHtml(_t('tasks.viewVulnerabilitiesQueueTitle'))}" aria-label="${escapeHtml(_t('tasks.viewVulnerabilitiesQueueTitle'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg></button>
|
||||
${canDelete ? `<button type="button" class="batch-queue-icon-btn batch-queue-icon-btn--danger" onclick="deleteBatchQueueFromList('${queue.id}')" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
|
||||
<button type="button" class="batch-queue-icon-btn" onclick="navigateToVulnerabilitiesFromTasksPage('queue', ${escapeJsStringAttr(queue.id)})" title="${escapeHtml(_t('tasks.viewVulnerabilitiesQueueTitle'))}" aria-label="${escapeHtml(_t('tasks.viewVulnerabilitiesQueueTitle'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg></button>
|
||||
${canDelete ? `<button type="button" class="batch-queue-icon-btn batch-queue-icon-btn--danger" onclick="deleteBatchQueueFromList(${escapeJsStringAttr(queue.id)})" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1835,7 +1847,7 @@ async function showBatchQueueDetail(queueId) {
|
||||
|
||||
deferModalContent(function () {
|
||||
content.innerHTML = `
|
||||
<div class="batch-queue-detail-layout" data-bq-detail-for="${escapeHtml(queue.id)}">
|
||||
<div class="batch-queue-detail-layout" data-bq-detail-for="${escapeAttr(queue.id)}">
|
||||
<section class="batch-queue-detail-hero">
|
||||
<span class="batch-queue-status ${pres.class}">${escapeHtml(pres.text)}</span>
|
||||
${pres.sublabel ? `<p class="batch-queue-detail-hero__sub">${escapeHtml(pres.sublabel)}</p>` : ''}
|
||||
@@ -1872,15 +1884,15 @@ async function showBatchQueueDetail(queueId) {
|
||||
const canEdit = allowSubtaskMutation && task.status !== 'running';
|
||||
const canRunSingle = batchQueueCanRunSingleTask(queue, task);
|
||||
const runSingleUnavailableTitle = escapeHtml(batchQueueRunSingleTaskDisabledReason(queue, task));
|
||||
const taskMessageEscaped = escapeHtml(task.message).replace(/'/g, "'").replace(/"/g, """).replace(/\n/g, "\\n");
|
||||
const taskMessageEscaped = escapeAttr(task.message).replace(/\n/g, "\\n");
|
||||
return `
|
||||
<div class="batch-task-item ${task.status === 'running' ? 'batch-task-item-active' : ''}" data-queue-id="${queue.id}" data-task-id="${task.id}" data-task-message="${taskMessageEscaped}">
|
||||
<div class="batch-task-item ${task.status === 'running' ? 'batch-task-item-active' : ''}" data-queue-id="${escapeAttr(queue.id)}" data-task-id="${escapeAttr(task.id)}" data-task-message="${taskMessageEscaped}">
|
||||
<div class="batch-task-header">
|
||||
<span class="batch-task-index">#${index + 1}</span>
|
||||
<span class="batch-task-status ${taskStatus.class}">${taskStatus.text}</span>
|
||||
<span class="batch-task-message" title="${escapeHtml(task.message)}">${escapeHtml(task.message)}</span>
|
||||
<button class="btn-secondary btn-small batch-task-run-btn" ${canRunSingle ? `onclick="runSingleBatchTask('${queue.id}', '${task.id}'); event.stopPropagation();"` : `disabled title="${runSingleUnavailableTitle}"`}>` + _t('tasks.runSingleTask') + `</button>
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
<span class="batch-task-message" title="${escapeAttr(task.message)}">${escapeHtml(task.message)}</span>
|
||||
<button class="btn-secondary btn-small batch-task-run-btn" ${canRunSingle ? `onclick="runSingleBatchTask(${escapeJsStringAttr(queue.id)}, ${escapeJsStringAttr(task.id)}); event.stopPropagation();"` : `disabled title="${runSingleUnavailableTitle}"`}>` + _t('tasks.runSingleTask') + `</button>
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation(${escapeJsStringAttr(task.conversationId)}); event.stopPropagation();">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
${canEdit ? `<button class="btn-secondary btn-small batch-task-edit-btn" onclick="editBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.edit') + `</button>` : ''}
|
||||
${canEdit ? `<button class="btn-secondary btn-small btn-danger batch-task-delete-btn" onclick="deleteBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.delete') + `</button>` : ''}
|
||||
</div>
|
||||
@@ -2182,7 +2194,7 @@ function editBatchTaskFromElement(button) {
|
||||
// 替换消息为内联编辑区域
|
||||
const editDiv = document.createElement('div');
|
||||
editDiv.className = 'batch-task-inline-edit';
|
||||
editDiv.innerHTML = `<textarea id="bq-task-edit-${escapeHtml(taskId)}">${escapeHtml(decodedMessage)}</textarea>`;
|
||||
editDiv.innerHTML = `<textarea id="bq-task-edit-${escapeAttr(taskId)}">${escapeHtml(decodedMessage)}</textarea>`;
|
||||
msgSpan.style.display = 'none';
|
||||
msgSpan.parentNode.insertBefore(editDiv, msgSpan.nextSibling);
|
||||
|
||||
@@ -2474,7 +2486,7 @@ function startInlineEditTitle() {
|
||||
const untitledText = _t('tasks.batchQueueUntitled');
|
||||
const val = currentTitle === untitledText ? '' : currentTitle;
|
||||
container.innerHTML = `<span class="bq-inline-edit-controls">
|
||||
<input type="text" id="bq-edit-title" value="${escapeHtml(val)}" placeholder="${escapeHtml(_t('batchImportModal.queueTitleHint') || '')}" style="width:180px;" />
|
||||
<input type="text" id="bq-edit-title" value="${escapeAttr(val)}" placeholder="${escapeAttr(_t('batchImportModal.queueTitleHint') || '')}" style="width:180px;" />
|
||||
</span>`;
|
||||
const inp = document.getElementById('bq-edit-title');
|
||||
if (inp) {
|
||||
@@ -2534,8 +2546,8 @@ function startInlineEditRole() {
|
||||
const currentRole = queue.role || '';
|
||||
const roles = (Array.isArray(batchQueuesState.loadedRoles) ? batchQueuesState.loadedRoles : []).filter(r => r.name !== '默认' && r.enabled !== false).sort((a, b) => (a.name || '').localeCompare(b.name || '', 'zh-CN'));
|
||||
const currentInList = !currentRole || roles.some(r => r.name === currentRole);
|
||||
const orphanOpt = !currentInList ? `<option value="${escapeHtml(currentRole)}" selected>${escapeHtml(currentRole)} (${escapeHtml(_t('batchQueueDetailModal.roleNotFound') || '已移除')})</option>` : '';
|
||||
const opts = roles.map(r => `<option value="${escapeHtml(r.name)}" ${r.name === currentRole ? 'selected' : ''}>${escapeHtml(r.name)}</option>`).join('');
|
||||
const orphanOpt = !currentInList ? `<option value="${escapeAttr(currentRole)}" selected>${escapeHtml(currentRole)} (${escapeHtml(_t('batchQueueDetailModal.roleNotFound') || '已移除')})</option>` : '';
|
||||
const opts = roles.map(r => `<option value="${escapeAttr(r.name)}" ${r.name === currentRole ? 'selected' : ''}>${escapeHtml(r.name)}</option>`).join('');
|
||||
container.innerHTML = `<span class="bq-inline-edit-controls">
|
||||
<select id="bq-edit-role">
|
||||
<option value="">${escapeHtml(_t('batchImportModal.defaultRole'))}</option>
|
||||
@@ -2762,7 +2774,7 @@ function startInlineEditSchedule() {
|
||||
<option value="manual" ${!isCron ? 'selected' : ''}>${escapeHtml(_t('batchImportModal.scheduleModeManual'))}</option>
|
||||
<option value="cron" ${isCron ? 'selected' : ''}>${escapeHtml(_t('batchImportModal.scheduleModeCron'))}</option>
|
||||
</select>
|
||||
<input type="text" id="bq-edit-cron-expr" class="bq-edit-cron-expr" value="${escapeHtml(queue.cronExpr || '')}" placeholder="${_t('batchImportModal.cronExprPlaceholder', { interpolation: { escapeValue: false } })}" style="${!isCron ? 'display:none;' : ''}" />
|
||||
<input type="text" id="bq-edit-cron-expr" class="bq-edit-cron-expr" value="${escapeAttr(queue.cronExpr || '')}" placeholder="${escapeAttr(_t('batchImportModal.cronExprPlaceholder', { interpolation: { escapeValue: false } }))}" style="${!isCron ? 'display:none;' : ''}" />
|
||||
</span>`;
|
||||
refreshBatchFormSelect('bq-edit-schedule-mode', { inline: true });
|
||||
let schedCancelled = false;
|
||||
|
||||
@@ -1385,18 +1385,19 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
|
||||
? escapeHtml(typeof getProjectName === 'function' ? getProjectName(vuln.project_id) : vuln.project_id)
|
||||
: escapeHtml(vulnT('vulnerabilityPage.projectUnbound'));
|
||||
const projectBadge = vuln.project_id
|
||||
? `<span class="vulnerability-project-badge" title="${escapeHtml(vuln.project_id)}">${escapeHtml(vulnT('vulnerabilityPage.detailProject'))}: ${projectLabel}</span>`
|
||||
? `<span class="vulnerability-project-badge" title="${escapeAttr(vuln.project_id)}">${escapeHtml(vulnT('vulnerabilityPage.detailProject'))}: ${projectLabel}</span>`
|
||||
: `<span class="vulnerability-project-badge vulnerability-project-badge--unbound">${escapeHtml(vulnT('vulnerabilityPage.projectUnbound'))}</span>`;
|
||||
const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle'));
|
||||
const editTitle = escapeHtml(vulnT('common.edit'));
|
||||
const deleteTitle = escapeHtml(vulnT('common.delete'));
|
||||
const vulnIdJs = escapeJsStringAttr(vuln.id);
|
||||
|
||||
return `
|
||||
<div class="vulnerability-card ${severityClass}" id="vulnerability-card-${vuln.id}" data-vuln-id="${escapeHtml(vuln.id)}">
|
||||
<div class="vulnerability-header" onclick="toggleVulnerabilityDetails('${vuln.id}')" style="cursor: pointer;">
|
||||
<div class="vulnerability-card ${severityClass}" id="vulnerability-card-${escapeAttr(vuln.id)}" data-vuln-id="${escapeAttr(vuln.id)}">
|
||||
<div class="vulnerability-header" onclick="toggleVulnerabilityDetails(${vulnIdJs})" style="cursor: pointer;">
|
||||
<div class="vulnerability-title-section">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<svg class="vulnerability-expand-icon" id="expand-icon-${vuln.id}" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="transition: transform 0.2s ease; flex-shrink: 0;">
|
||||
<svg class="vulnerability-expand-icon" id="expand-icon-${escapeAttr(vuln.id)}" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="transition: transform 0.2s ease; flex-shrink: 0;">
|
||||
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<h3 class="vulnerability-title">${escapeHtml(vuln.title)}</h3>
|
||||
@@ -1409,20 +1410,20 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="vulnerability-actions" onclick="event.stopPropagation();">
|
||||
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="${dlTitle}">
|
||||
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown(${vulnIdJs}, event)" title="${dlTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="7 10 12 15 17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="${editTitle}">
|
||||
<button class="btn-ghost" onclick="editVulnerability(${vulnIdJs})" title="${editTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-ghost" data-require-permission="vulnerability:delete" onclick="deleteVulnerability('${vuln.id}')" title="${deleteTitle}">
|
||||
<button class="btn-ghost" data-require-permission="vulnerability:delete" onclick="deleteVulnerability(${vulnIdJs})" title="${deleteTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -1450,7 +1451,7 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRecommendation'), vuln.recommendation)}
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestNotes'), vuln.retest_notes)}
|
||||
</div>
|
||||
<div class="vulnerability-related-facts" id="vuln-related-facts-${vuln.id}" data-project-id="${escapeHtml(vuln.project_id || '')}" data-vuln-id="${escapeHtml(vuln.id)}" hidden></div>
|
||||
<div class="vulnerability-related-facts" id="vuln-related-facts-${escapeAttr(vuln.id)}" data-project-id="${escapeAttr(vuln.project_id || '')}" data-vuln-id="${escapeAttr(vuln.id)}" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1557,10 +1558,10 @@ function buildVulnerabilityProjectOptionsHtml(selectedId) {
|
||||
entries.forEach(([id, name]) => {
|
||||
if (!id) return;
|
||||
const selected = id === sel ? ' selected' : '';
|
||||
html += `<option value="${escapeHtml(id)}"${selected}>${escapeHtml(name || id)}</option>`;
|
||||
html += `<option value="${escapeAttr(id)}"${selected}>${escapeHtml(name || id)}</option>`;
|
||||
});
|
||||
if (sel && !entries.some(([id]) => id === sel)) {
|
||||
html += `<option value="${escapeHtml(sel)}" selected>${escapeHtml(sel)}</option>`;
|
||||
html += `<option value="${escapeAttr(sel)}" selected>${escapeHtml(sel)}</option>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
@@ -1974,8 +1975,8 @@ async function loadVulnerabilityRelatedFacts(vulnId) {
|
||||
.map((f) => {
|
||||
const key = escapeHtml(f.fact_key);
|
||||
const sum = escapeHtml((f.summary || '').slice(0, 120));
|
||||
const pid = escapeHtml(projectId);
|
||||
const rawKey = escapeHtml(f.fact_key);
|
||||
const pid = escapeAttr(projectId);
|
||||
const rawKey = escapeAttr(f.fact_key);
|
||||
return `<li><a role="button" href="#" data-project-id="${pid}" data-fact-key="${rawKey}" onclick="event.preventDefault();openProjectFactFromVulnerability(this.dataset.projectId,this.dataset.factKey)"><code>${key}</code></a> — ${sum}</li>`;
|
||||
})
|
||||
.join('');
|
||||
@@ -2016,6 +2017,18 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return JSON.stringify(String(text == null ? '' : text));
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeJsStringAttr(text) {
|
||||
return escapeAttr(escapeJsString(text));
|
||||
}
|
||||
|
||||
/** 复制详情字段(编码由 encodeURIComponent 传入,避免引号截断) */
|
||||
function vulnerabilityCopyEncoded(evt, encoded) {
|
||||
if (evt && evt.stopPropagation) {
|
||||
@@ -2076,9 +2089,9 @@ function vulnDetailProjectField(vuln) {
|
||||
return `<div class="vuln-detail-field">
|
||||
<div class="vuln-detail-field__label">${escapeHtml(label)}</div>
|
||||
<div class="vuln-detail-field__row">
|
||||
<select class="vulnerability-project-bind-select" data-vuln-id="${escapeHtml(vuln.id)}"
|
||||
<select class="vulnerability-project-bind-select" data-vuln-id="${escapeAttr(vuln.id)}"
|
||||
onchange="bindVulnerabilityProject(this.dataset.vulnId, this.value, true)"
|
||||
title="${hint}" aria-label="${escapeHtml(label)}">
|
||||
title="${escapeAttr(hint)}" aria-label="${escapeAttr(label)}">
|
||||
${buildVulnerabilityProjectOptionsHtml(vuln.project_id || '')}
|
||||
</select>
|
||||
<span class="vuln-detail-field__copy-spacer" aria-hidden="true"></span>
|
||||
@@ -2450,7 +2463,7 @@ async function refreshVulnerabilityProjectFilter() {
|
||||
if (!p.id) return;
|
||||
const selected = p.id === cur ? ' selected' : '';
|
||||
const arch = p.status === 'archived' ? ' [' + vulnT('projects.archived') + ']' : '';
|
||||
html += `<option value="${escapeHtml(p.id)}"${selected}>${escapeHtml(p.name || p.id)}${arch}</option>`;
|
||||
html += `<option value="${escapeAttr(p.id)}"${selected}>${escapeHtml(p.name || p.id)}${arch}</option>`;
|
||||
});
|
||||
sel.innerHTML = html;
|
||||
if (cur) sel.value = cur;
|
||||
|
||||
Reference in New Issue
Block a user