Add files via upload

This commit is contained in:
公明
2026-07-28 18:15:54 +08:00
committed by GitHub
parent e0965594bb
commit 52595e07e5
6 changed files with 315 additions and 4 deletions
+68
View File
@@ -3990,6 +3990,17 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
border-top-left-radius: 2px;
}
.message.assistant.assistant-not-finalized .message-bubble {
border-color: rgba(245, 124, 0, 0.28);
border-left: 4px solid #f57c00;
background: linear-gradient(90deg, rgba(245, 124, 0, 0.08), rgba(245, 124, 0, 0.035));
box-shadow: 0 6px 18px rgba(245, 124, 0, 0.08);
}
.message.assistant.assistant-not-finalized .message-bubble strong:first-child {
color: #b45309;
}
.message.assistant .message-bubble pre {
margin: 0;
white-space: pre-wrap;
@@ -7381,6 +7392,16 @@ html[data-theme="dark"] .login-card .login-submit:disabled {
background: rgba(245, 124, 0, 0.09);
}
.timeline-item-finalization_check {
border-left-color: #f57c00;
background: linear-gradient(90deg, rgba(245, 124, 0, 0.1), rgba(96, 125, 139, 0.045));
}
.timeline-item-finalization_check .timeline-item-title {
color: #9a5200;
font-weight: 600;
}
.timeline-item-tool_calls_detected {
border-left-color: #0277bd;
background: rgba(2, 119, 189, 0.06);
@@ -22967,6 +22988,18 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
font-weight: 500;
color: var(--text-secondary);
}
.webshell-ai-timeline-finalization_check {
border-left: 3px solid #f57c00;
padding-left: 10px;
background: linear-gradient(90deg, rgba(245, 124, 0, 0.08), transparent);
}
.webshell-ai-timeline-finalization_check .webshell-ai-timeline-title {
color: #9a5200;
font-weight: 600;
}
.webshell-ai-timeline-msg {
margin-top: 4px;
padding-left: 0;
@@ -23144,6 +23177,11 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.webshell-ai-msg.assistant.webshell-ai-candidate-output {
border-color: rgba(245, 124, 0, 0.28);
background: linear-gradient(90deg, rgba(245, 124, 0, 0.08), rgba(245, 124, 0, 0.035));
box-shadow: 0 6px 16px rgba(245, 124, 0, 0.08);
}
.webshell-ai-msg.assistant.webshell-ai-msg-error {
max-width: 72%;
border-color: rgba(220, 53, 69, 0.35);
@@ -36232,12 +36270,42 @@ html[data-theme="dark"] .timeline-item-user_interrupt_continue {
background: rgba(251, 191, 36, 0.08);
}
html[data-theme="dark"] .timeline-item-finalization_check {
background: linear-gradient(90deg, rgba(251, 191, 36, 0.1), rgba(30, 41, 59, 0.18));
border-left-color: #fbbf24;
}
html[data-theme="dark"] .timeline-item-finalization_check .timeline-item-title,
html[data-theme="dark"] .webshell-ai-timeline-finalization_check .webshell-ai-timeline-title {
color: #fbbf24;
}
html[data-theme="dark"] .message.assistant .message-bubble {
background: #111827;
color: var(--text-primary);
border-color: var(--border-color);
}
html[data-theme="dark"] .message.assistant.assistant-not-finalized .message-bubble {
border-color: rgba(251, 191, 36, 0.24);
border-left-color: #fbbf24;
background: linear-gradient(90deg, rgba(251, 191, 36, 0.08), rgba(17, 24, 39, 0.9));
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22);
}
html[data-theme="dark"] .message.assistant.assistant-not-finalized .message-bubble strong:first-child {
color: #fbbf24;
}
html[data-theme="dark"] .webshell-ai-timeline-finalization_check {
border-left-color: #fbbf24;
background: linear-gradient(90deg, rgba(251, 191, 36, 0.08), rgba(17, 24, 39, 0.12));
}
html[data-theme="dark"] .webshell-ai-msg.assistant.webshell-ai-candidate-output {
background: linear-gradient(90deg, rgba(251, 191, 36, 0.08), rgba(17, 24, 39, 0.12));
}
html[data-theme="dark"] .message-copy-btn {
background: #1f2937;
border-color: #334155;
+1
View File
@@ -1147,6 +1147,7 @@ async function sendAssetsToChat(assets, template) {
input.value = message;
if (typeof adjustTextareaHeight === 'function') adjustTextareaHeight(input);
// 消息流可能持续很久;启动发送即可返回,让提交弹窗立即关闭。
window.__csNextChatFinalizationPolicy = { requireExecutionEvidence: true };
void sendMessage();
}
+46
View File
@@ -80,6 +80,7 @@ let chatAttachmentSeq = 0;
// 对话模式:eino_single = Eino ADK 单代理(/api/eino-agent/stream);deep / plan_execute / supervisor = Eino 多代理(/api/multi-agent/stream,请求体 orchestration
const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode';
const AGENT_MODE_CONVERSATION_STORAGE_PREFIX = 'cyberstrike-chat-agent-mode:conversation';
const AI_CHANNEL_STORAGE_KEY = 'cyberstrike-chat-ai-channel';
const REASONING_MODE_LS = 'cyberstrike-chat-reasoning-mode';
const REASONING_EFFORT_LS = 'cyberstrike-chat-reasoning-effort';
@@ -733,6 +734,44 @@ function chatAgentModeNormalizeStored(stored, cfg) {
return CHAT_AGENT_MODE_EINO_SINGLE;
}
function normalizeConversationAgentModeForUI(mode) {
const v = String(mode || '').trim().toLowerCase().replace(/-/g, '_');
if (chatAgentModeIsEinoSingle(v)) return v;
if (chatAgentModeIsEino(v)) {
return multiAgentAPIEnabled ? v : CHAT_AGENT_MODE_EINO_SINGLE;
}
return '';
}
function conversationAgentModeStorageKey(conversationId) {
return `${AGENT_MODE_CONVERSATION_STORAGE_PREFIX}:${String(conversationId || '').trim()}`;
}
function readConversationAgentModePreference(conversationId) {
if (!conversationId) return '';
try {
return normalizeConversationAgentModeForUI(localStorage.getItem(conversationAgentModeStorageKey(conversationId)) || '');
} catch (e) {
return '';
}
}
function saveConversationAgentModePreference(conversationId, mode) {
const normalized = normalizeConversationAgentModeForUI(mode);
if (!conversationId || !normalized) return;
try {
localStorage.setItem(conversationAgentModeStorageKey(conversationId), normalized);
} catch (e) { /* ignore */ }
}
function applyConversationAgentMode(conversationId, conversation) {
const saved = readConversationAgentModePreference(conversationId);
const fromServer = normalizeConversationAgentModeForUI(conversation && (conversation.agentMode || conversation.agent_mode));
const mode = saved || fromServer;
if (!mode) return;
syncAgentModeFromValue(mode);
}
if (typeof window !== 'undefined') {
window.csaiHitlGlobalToolWhitelist = window.csaiHitlGlobalToolWhitelist || [];
window.csaiHitlDefaultReviewer = window.csaiHitlDefaultReviewer || 'human';
@@ -1098,6 +1137,7 @@ function toggleAgentModePanel() {
function selectAgentMode(mode) {
const ok = chatAgentModeIsEinoSingle(mode) || chatAgentModeIsEino(mode);
if (!ok) return;
saveConversationAgentModePreference(currentConversationId, mode);
try {
localStorage.setItem(AGENT_MODE_STORAGE_KEY, mode);
} catch (e) { /* ignore */ }
@@ -1351,6 +1391,10 @@ async function sendMessage() {
conversationId: currentConversationId,
role: typeof getCurrentRole === 'function' ? getCurrentRole() : ''
};
if (window.__csNextChatFinalizationPolicy && typeof window.__csNextChatFinalizationPolicy === 'object') {
body.finalization = window.__csNextChatFinalizationPolicy;
window.__csNextChatFinalizationPolicy = null;
}
let streamConversationId = body.conversationId ? String(body.conversationId) : null;
const isStreamStillVisibleForRequest = function () {
if (!document.getElementById(progressId)) return false;
@@ -1405,6 +1449,7 @@ async function sendMessage() {
try {
const modeSel = document.getElementById('agent-mode-select');
let modeVal = modeSel ? modeSel.value : CHAT_AGENT_MODE_EINO_SINGLE;
saveConversationAgentModePreference(streamConversationId || currentConversationId, modeVal);
const useMulti = multiAgentAPIEnabled && chatAgentModeIsEino(modeVal);
const streamPath = useMulti ? '/api/multi-agent/stream' : '/api/eino-agent/stream';
if (useMulti && modeVal) {
@@ -4845,6 +4890,7 @@ async function loadConversation(conversationId) {
if (typeof window.setCurrentRole === 'function') {
window.setCurrentRole(conversationRoleName || '默认');
}
applyConversationAgentMode(conversationId, conversation);
try {
window.currentConversationId = conversationId;
} catch (e) { /* ignore */ }
+1
View File
@@ -1334,6 +1334,7 @@ function scanFofaRow(encodedRowJson, clickEvent) {
}
if (autoSend) {
if (typeof sendMessage === 'function') {
window.__csNextChatFinalizationPolicy = { requireExecutionEvidence: true };
sendMessage();
} else {
alert(_t('infoCollect.noSendMessage'));
+122 -3
View File
@@ -198,6 +198,92 @@ function resolveFinalAssistantResponseText(finalMessage, streamState) {
return finalMessage;
}
function isFinalizedResponseData(data) {
return !!(data && data.finalized === true);
}
function hasFinalizationContract(data) {
if (!data || typeof data !== 'object') return false;
return Object.prototype.hasOwnProperty.call(data, 'finalized')
|| Object.prototype.hasOwnProperty.call(data, 'finalizable')
|| Object.prototype.hasOwnProperty.call(data, 'completionReason')
|| Object.prototype.hasOwnProperty.call(data, 'evidenceVerified')
|| Object.prototype.hasOwnProperty.call(data, 'missingChecks');
}
function finalizationCheckTitle(data) {
return isFinalizedResponseData(data) ? '最终回复检查通过' : '最终回复检查未通过';
}
function finalizationReasonLabel(reason, status) {
const key = String(reason || status || '').trim();
const labels = {
pending_tool_executions: '等待工具执行完成',
missing_execution_evidence: '缺少完成态证据',
awaiting_hitl: '等待人工确认',
empty_response: '未捕获到有效回复',
missing_finalization_contract: '缺少最终化证明',
in_progress: '仍在验证',
blocked: '检查未通过',
failed: '任务失败',
cancelled: '任务已取消',
verified: '已验证'
};
return labels[key] || key || '检查未通过';
}
function finalizationMissingCheckLabel(check) {
const s = String(check || '').trim();
if (!s) return '';
if (s.indexOf('tool execution still queued or running') !== -1) return '仍有工具执行未结束';
if (s.indexOf('execution evidence is required but no completed tool execution was recorded') !== -1) return '本轮要求执行证据,但没有 completed 工具记录';
if (s.indexOf('workflow is awaiting HITL approval') !== -1) return '工作流正在等待人工确认';
if (s.indexOf('assistant final text is empty') !== -1) return '未捕获到有效最终文本';
if (s.indexOf('agent run status is ') === 0) return '任务状态仍为 ' + s.replace('agent run status is ', '');
return s;
}
function compactStringList(values, limit) {
const arr = Array.isArray(values) ? values.filter(Boolean).map(String) : [];
const max = limit || 3;
if (arr.length <= max) return arr;
return arr.slice(0, max).concat('另 ' + (arr.length - max) + ' 项');
}
function finalizationNoticeMarkdown(responseData, eventMessage) {
const hasContract = hasFinalizationContract(responseData);
const reason = hasContract
? finalizationReasonLabel(responseData && responseData.completionReason, responseData && responseData.status)
: finalizationReasonLabel('missing_finalization_contract');
const lines = ['**仍在验证,暂不生成最终结论**', '', '状态:' + reason];
const pending = compactStringList(responseData && responseData.pendingExecutionIds, 3);
if (pending.length) {
lines.push('待完成工具:`' + pending.join('`, `') + '`');
}
const rawMissingChecks = responseData && responseData.missingChecks;
const missingChecks = Array.isArray(rawMissingChecks)
? rawMissingChecks
: (rawMissingChecks ? [rawMissingChecks] : []);
const missing = compactStringList(missingChecks.map(finalizationMissingCheckLabel).filter(Boolean), 3);
if (missing.length) {
lines.push('待完成检查:' + missing.join(''));
}
if (!hasContract && eventMessage != null && String(eventMessage).trim() !== '') {
lines.push('', '候选输出已移入过程详情,避免误判为最终结论。');
}
return lines.join('\n');
}
function markAssistantFinalizationState(assistantMessageId, responseData) {
const assistantElement = document.getElementById(assistantMessageId);
if (!assistantElement) return;
const finalized = isFinalizedResponseData(responseData);
assistantElement.dataset.finalized = finalized ? 'true' : 'false';
assistantElement.dataset.finalizationStatus = responseData && responseData.status ? String(responseData.status) : '';
assistantElement.classList.toggle('assistant-finalized', finalized);
assistantElement.classList.toggle('assistant-not-finalized', !finalized);
}
/**
* 主通道 response 结束时将流式占位条目固化为 planning与后端 flushResponsePlan 落库类型一致
* 避免 integrateProgressToMCPSection 快照前删除占位导致助手输出仅刷新后才出现
@@ -2440,6 +2526,26 @@ function handleStreamEvent(event, progressElement, progressId,
});
break;
case 'finalization_check':
const finalizationCheckData = event.data || {};
const finalizationCheckPassed = isFinalizedResponseData(finalizationCheckData);
addTimelineItem(timeline, 'finalization_check', {
title: finalizationCheckTitle(finalizationCheckData),
message: finalizationCheckPassed ? (event.message || '最终回复检查通过。') : finalizationNoticeMarkdown(finalizationCheckData, event.message),
data: event.data,
expanded: !finalizationCheckPassed
});
break;
case 'finalization_auto_continue':
addTimelineItem(timeline, 'progress', {
title: '继续验证',
message: event.message,
data: event.data,
expanded: false
});
break;
case 'hitl_interrupt':
const hitlTargetItem = findToolCallItemForHitl(timeline, event.data || {});
if (hitlTargetItem && hitlTargetItem.id) {
@@ -2958,7 +3064,12 @@ function handleStreamEvent(event, progressElement, progressId,
const streamState = responseStreamStateByProgressId.get(progressId);
const existingAssistantId = streamState?.assistantId || getAssistantId();
let assistantIdFinal = existingAssistantId;
const bubbleText = resolveFinalAssistantResponseText(event.message, streamState);
const responseFinalized = isFinalizedResponseData(responseData);
const responseHasFinalizationContract = hasFinalizationContract(responseData);
const resolvedResponseText = resolveFinalAssistantResponseText(event.message, streamState);
const bubbleText = responseFinalized
? resolvedResponseText
: finalizationNoticeMarkdown(responseData, event.message);
if (!assistantIdFinal) {
assistantIdFinal = addMessage('assistant', bubbleText, mcpIds, progressId);
@@ -2967,11 +3078,12 @@ function handleStreamEvent(event, progressElement, progressId,
setAssistantId(assistantIdFinal);
updateAssistantBubbleContent(assistantIdFinal, bubbleText, true);
}
markAssistantFinalizationState(assistantIdFinal, responseData);
// 将 response_start/response_delta 占位固化为 planning,与后端落库一致后再快照过程详情
if (streamState && streamState.itemId) {
finalizeMainResponseStreamItem(streamState, event.message, responseData);
} else if (timeline && bubbleText && String(bubbleText).trim() && !isEinoEmptyResponsePlaceholder(event.message)) {
finalizeMainResponseStreamItem(streamState, responseFinalized ? event.message : '', responseData);
} else if (timeline && responseFinalized && bubbleText && String(bubbleText).trim() && !isEinoEmptyResponsePlaceholder(event.message)) {
addTimelineItem(timeline, 'planning', {
title: typeof einoMainStreamPlanningTitle === 'function'
? einoMainStreamPlanningTitle(responseData)
@@ -2980,6 +3092,13 @@ function handleStreamEvent(event, progressElement, progressId,
data: responseData,
expanded: false
});
} else if (timeline && !responseFinalized && !responseHasFinalizationContract && resolvedResponseText && String(resolvedResponseText).trim()) {
addTimelineItem(timeline, 'finalization_check', {
title: '候选输出缺少最终化证明',
message: resolvedResponseText,
data: Object.assign({}, responseData, { missingFinalizationContract: true }),
expanded: true
});
}
// 最终回复时隐藏进度卡片(多代理模式下,迭代过程已完整展示)
+77 -1
View File
@@ -1338,6 +1338,55 @@ function escapeHtmlAttr(s) {
return escapeHtml(s).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function webshellFinalizationReasonLabel(reason, status) {
var key = String(reason || status || '').trim();
var labels = {
pending_tool_executions: '等待工具执行完成',
missing_execution_evidence: '缺少完成态证据',
awaiting_hitl: '等待人工确认',
empty_response: '未捕获到有效回复',
missing_finalization_contract: '缺少最终化证明',
in_progress: '仍在验证',
blocked: '检查未通过',
failed: '任务失败',
cancelled: '任务已取消',
verified: '已验证'
};
return labels[key] || key || '检查未通过';
}
function webshellFinalizationMissingCheckLabel(check) {
var s = String(check || '').trim();
if (!s) return '';
if (s.indexOf('tool execution still queued or running') !== -1) return '仍有工具执行未结束';
if (s.indexOf('execution evidence is required but no completed tool execution was recorded') !== -1) return '本轮要求执行证据,但没有 completed 工具记录';
if (s.indexOf('workflow is awaiting HITL approval') !== -1) return '工作流正在等待人工确认';
if (s.indexOf('assistant final text is empty') !== -1) return '未捕获到有效最终文本';
if (s.indexOf('agent run status is ') === 0) return '任务状态仍为 ' + s.replace('agent run status is ', '');
return s;
}
function webshellFinalizationNotice(data, eventMessage, hasContract) {
var reason = hasContract
? webshellFinalizationReasonLabel(data && data.completionReason, data && data.status)
: webshellFinalizationReasonLabel('missing_finalization_contract');
var lines = ['仍在验证,暂不生成最终结论', '状态:' + reason];
var pending = Array.isArray(data && data.pendingExecutionIds) ? data.pendingExecutionIds.filter(Boolean).map(String) : [];
if (pending.length) {
lines.push('待完成工具:' + pending.slice(0, 3).join(', ') + (pending.length > 3 ? ' 等' : ''));
}
var missing = Array.isArray(data && data.missingChecks)
? data.missingChecks.map(webshellFinalizationMissingCheckLabel).filter(Boolean)
: [];
if (missing.length) {
lines.push('待完成检查:' + missing.slice(0, 2).join('') + (missing.length > 2 ? ' 等' : ''));
}
if (!hasContract && eventMessage) {
lines.push('候选输出已移入过程详情。');
}
return lines.join('\n');
}
function escapeSingleQuotedShellArg(value) {
var s = value == null ? '' : String(value);
return "'" + s.replace(/'/g, "'\\''") + "'";
@@ -3393,7 +3442,10 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
message: message,
webshellConnectionId: conn.id,
conversationId: convId,
role: wsRole
role: wsRole,
finalization: {
requireExecutionEvidence: true
}
};
if (!convId) {
var wsPid = getWebshellAiProjectSelection(conn);
@@ -3465,6 +3517,8 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
streamingTarget = '';
webshellStreamingTypingId += 1;
streamingTypingId = webshellStreamingTypingId;
assistantDiv.dataset.finalized = 'false';
assistantDiv.classList.add('webshell-ai-candidate-output');
assistantDiv.textContent = '…';
messagesContainer.scrollTop = messagesContainer.scrollHeight;
} else if (_et === 'response_delta') {
@@ -3485,6 +3539,23 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
}
} else if (_et === 'response') {
var text = (_em != null && _em !== '') ? _em : (typeof _ed === 'string' ? _ed : '');
var finalized = !!(_ed && _ed.finalized === true);
var hasFinalizationContract = !!(_ed && (
Object.prototype.hasOwnProperty.call(_ed, 'finalized') ||
Object.prototype.hasOwnProperty.call(_ed, 'finalizable') ||
Object.prototype.hasOwnProperty.call(_ed, 'completionReason') ||
Object.prototype.hasOwnProperty.call(_ed, 'evidenceVerified') ||
Object.prototype.hasOwnProperty.call(_ed, 'missingChecks')
));
assistantDiv.dataset.finalized = finalized ? 'true' : 'false';
assistantDiv.classList.toggle('webshell-ai-candidate-output', !finalized);
assistantDiv.classList.toggle('webshell-ai-finalized-output', finalized);
if (!finalized && !hasFinalizationContract && text) {
appendTimelineItem('finalization_check', '候选输出缺少最终化证明', text, Object.assign({}, _ed || {}, { missingFinalizationContract: true }));
text = webshellFinalizationNotice(_ed || {}, text, false);
} else if (!finalized && hasFinalizationContract) {
text = webshellFinalizationNotice(_ed || {}, text, true);
}
if (text) {
streamingTarget = String(text);
webshellStreamingTypingId += 1;
@@ -3493,6 +3564,11 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
}
// ─── Terminal events ───
} else if (_et === 'finalization_check') {
var finalizationOk = !!(_ed && _ed.finalized === true);
appendTimelineItem('finalization_check', finalizationOk ? '最终回复检查通过' : '最终回复检查未通过', finalizationOk ? (_em || '最终回复检查通过。') : webshellFinalizationNotice(_ed || {}, _em, true), _ed);
} else if (_et === 'finalization_auto_continue') {
appendTimelineItem('progress', '继续验证', _em, _ed);
} else if (_et === 'error' && _em) {
streamingTypingId += 1;
var errLabel = wsTOr('chat.error', '错误');