mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96564d4d89 | |||
| d85afa2d39 | |||
| 55b6bceb21 | |||
| 65d73b3d66 | |||
| 913115d1fb | |||
| e1b967d781 | |||
| 9d9efa886f | |||
| cae45e9dc5 |
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="web/static/logo.png" alt="CyberStrikeAI Logo" width="200">
|
<img src="images/logo.png" alt="CyberStrikeAI Logo" width="200">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# CyberStrikeAI
|
# CyberStrikeAI
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="web/static/logo.png" alt="CyberStrikeAI Logo" width="200">
|
<img src="images/logo.png" alt="CyberStrikeAI Logo" width="200">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# CyberStrikeAI
|
# CyberStrikeAI
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||||
version: "v1.5.7"
|
version: "v1.5.8"
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -91,6 +91,20 @@ func DefaultSingleAgentSystemPrompt() string {
|
|||||||
|
|
||||||
当工具返回错误时,错误信息会包含在工具响应中,请仔细阅读并做出合理的决策。
|
当工具返回错误时,错误信息会包含在工具响应中,请仔细阅读并做出合理的决策。
|
||||||
|
|
||||||
|
## 结束条件与停止约束
|
||||||
|
|
||||||
|
- 在「未完成用户目标」前,不得输出纯计划/纯建议式结论并结束本轮;必须继续给出可执行下一步,并优先通过工具验证。
|
||||||
|
- 若你准备结束回答,先执行一次自检:
|
||||||
|
1) 是否已有可验证证据支撑“任务完成/无法继续”的结论;
|
||||||
|
2) 是否至少尝试过当前路径的合理替代(参数、路径、方法、入口);
|
||||||
|
3) 是否仍存在可执行且低成本的下一步验证动作。
|
||||||
|
- 仅当满足以下任一条件时,才允许输出最终收尾:
|
||||||
|
1) 已达到用户目标并给出证据;
|
||||||
|
2) 达到明确边界(超时、权限、目标不可达、工具不可用且无替代),并清楚说明阻断点与已尝试项;
|
||||||
|
3) 用户明确要求停止。
|
||||||
|
- 若最近一步得到 404/空结果/无效响应,不得直接结束;至少再进行一次“同目标不同策略”的验证(如变更路径、参数、请求方法、上下文来源)。
|
||||||
|
- 避免无效空转:同一工具+同类参数连续失败 3 次后,必须切换策略(改工具、改入口、改假设)并说明切换原因。
|
||||||
|
|
||||||
## 漏洞记录
|
## 漏洞记录
|
||||||
|
|
||||||
发现有效漏洞时,必须使用 ` + builtin.ToolRecordVulnerability + ` 记录:标题、描述、严重程度、类型、目标、证明(POC)、影响、修复建议。
|
发现有效漏洞时,必须使用 ` + builtin.ToolRecordVulnerability + ` 记录:标题、描述、严重程度、类型、目标、证明(POC)、影响、修复建议。
|
||||||
|
|||||||
@@ -57,19 +57,30 @@ func newEinoSummarizationMiddleware(
|
|||||||
if modelName == "" {
|
if modelName == "" {
|
||||||
modelName = "gpt-4o"
|
modelName = "gpt-4o"
|
||||||
}
|
}
|
||||||
|
tokenCounter := einoSummarizationTokenCounter(modelName)
|
||||||
|
recentTrailMax := trigger / 4
|
||||||
|
if recentTrailMax < 2048 {
|
||||||
|
recentTrailMax = 2048
|
||||||
|
}
|
||||||
|
if recentTrailMax > trigger/2 {
|
||||||
|
recentTrailMax = trigger / 2
|
||||||
|
}
|
||||||
|
|
||||||
mw, err := summarization.New(ctx, &summarization.Config{
|
mw, err := summarization.New(ctx, &summarization.Config{
|
||||||
Model: summaryModel,
|
Model: summaryModel,
|
||||||
Trigger: &summarization.TriggerCondition{
|
Trigger: &summarization.TriggerCondition{
|
||||||
ContextTokens: trigger,
|
ContextTokens: trigger,
|
||||||
},
|
},
|
||||||
TokenCounter: einoSummarizationTokenCounter(modelName),
|
TokenCounter: tokenCounter,
|
||||||
UserInstruction: einoSummarizeUserInstruction,
|
UserInstruction: einoSummarizeUserInstruction,
|
||||||
EmitInternalEvents: false,
|
EmitInternalEvents: false,
|
||||||
PreserveUserMessages: &summarization.PreserveUserMessages{
|
PreserveUserMessages: &summarization.PreserveUserMessages{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
MaxTokens: preserveMax,
|
MaxTokens: preserveMax,
|
||||||
},
|
},
|
||||||
|
Finalize: func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) {
|
||||||
|
return summarizeFinalizeWithRecentAssistantToolTrail(ctx, originalMessages, summary, tokenCounter, recentTrailMax)
|
||||||
|
},
|
||||||
Callback: func(ctx context.Context, before, after adk.ChatModelAgentState) error {
|
Callback: func(ctx context.Context, before, after adk.ChatModelAgentState) error {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -89,6 +100,108 @@ func newEinoSummarizationMiddleware(
|
|||||||
return mw, nil
|
return mw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// summarizeFinalizeWithRecentAssistantToolTrail 在摘要消息后保留最近 assistant/tool 轨迹,避免压缩后执行链断裂。
|
||||||
|
func summarizeFinalizeWithRecentAssistantToolTrail(
|
||||||
|
ctx context.Context,
|
||||||
|
originalMessages []adk.Message,
|
||||||
|
summary adk.Message,
|
||||||
|
tokenCounter summarization.TokenCounterFunc,
|
||||||
|
recentTrailTokenBudget int,
|
||||||
|
) ([]adk.Message, error) {
|
||||||
|
systemMsgs := make([]adk.Message, 0, len(originalMessages))
|
||||||
|
nonSystem := make([]adk.Message, 0, len(originalMessages))
|
||||||
|
for _, msg := range originalMessages {
|
||||||
|
if msg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if msg.Role == schema.System {
|
||||||
|
systemMsgs = append(systemMsgs, msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nonSystem = append(nonSystem, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if recentTrailTokenBudget <= 0 || len(nonSystem) == 0 {
|
||||||
|
out := make([]adk.Message, 0, len(systemMsgs)+1)
|
||||||
|
out = append(out, systemMsgs...)
|
||||||
|
out = append(out, summary)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedReverse := make([]adk.Message, 0, 8)
|
||||||
|
seen := make(map[adk.Message]struct{})
|
||||||
|
totalTokens := 0
|
||||||
|
assistantToolKept := 0
|
||||||
|
const minAssistantToolTrail = 4
|
||||||
|
|
||||||
|
tryKeep := func(msg adk.Message) (bool, error) {
|
||||||
|
if msg == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if _, ok := seen[msg]; ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
n, err := tokenCounter(ctx, &summarization.TokenCounterInput{Messages: []adk.Message{msg}})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if n <= 0 {
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
if totalTokens+n > recentTrailTokenBudget {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
totalTokens += n
|
||||||
|
selectedReverse = append(selectedReverse, msg)
|
||||||
|
seen[msg] = struct{}{}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先保留最近 assistant/tool,确保执行轨迹可续跑。
|
||||||
|
for i := len(nonSystem) - 1; i >= 0; i-- {
|
||||||
|
msg := nonSystem[i]
|
||||||
|
if msg.Role != schema.Assistant && msg.Role != schema.Tool {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok, err := tryKeep(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
assistantToolKept++
|
||||||
|
}
|
||||||
|
if assistantToolKept >= minAssistantToolTrail {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在预算内回填更多最近消息,保持短链路上下文。
|
||||||
|
for i := len(nonSystem) - 1; i >= 0; i-- {
|
||||||
|
_, exists := seen[nonSystem[i]]
|
||||||
|
if exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok, err := tryKeep(nonSystem[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := make([]adk.Message, 0, len(selectedReverse))
|
||||||
|
for i := len(selectedReverse) - 1; i >= 0; i-- {
|
||||||
|
selected = append(selected, selectedReverse[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]adk.Message, 0, len(systemMsgs)+1+len(selected))
|
||||||
|
out = append(out, systemMsgs...)
|
||||||
|
out = append(out, summary)
|
||||||
|
out = append(out, selected...)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func einoSummarizationTokenCounter(openAIModel string) summarization.TokenCounterFunc {
|
func einoSummarizationTokenCounter(openAIModel string) summarization.TokenCounterFunc {
|
||||||
tc := agent.NewTikTokenCounter()
|
tc := agent.NewTikTokenCounter()
|
||||||
return func(ctx context.Context, input *summarization.TokenCounterInput) (int, error) {
|
return func(ctx context.Context, input *summarization.TokenCounterInput) (int, error) {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 85 KiB |
+62
-37
@@ -765,50 +765,59 @@ async function sendMessage() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('请求失败: ' + response.status);
|
throw new Error('请求失败: ' + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
window.__csAgentLiveStream = {
|
||||||
const decoder = new TextDecoder();
|
active: true,
|
||||||
let buffer = '';
|
conversationId: currentConversationId || null,
|
||||||
|
progressId: progressId
|
||||||
while (true) {
|
};
|
||||||
const { done, value } = await reader.read();
|
try {
|
||||||
if (done) break;
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
buffer += decoder.decode(value, { stream: true });
|
let buffer = '';
|
||||||
const lines = buffer.split('\n');
|
|
||||||
buffer = lines.pop(); // 保留最后一个不完整的行
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
for (const line of lines) {
|
if (done) break;
|
||||||
if (line.startsWith('data: ')) {
|
|
||||||
try {
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const eventData = JSON.parse(line.slice(6));
|
const lines = buffer.split('\n');
|
||||||
handleStreamEvent(eventData, progressElement, progressId,
|
buffer = lines.pop(); // 保留最后一个不完整的行
|
||||||
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
|
||||||
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
for (const line of lines) {
|
||||||
} catch (e) {
|
if (line.startsWith('data: ')) {
|
||||||
console.error('解析事件数据失败:', e, line);
|
try {
|
||||||
|
const eventData = JSON.parse(line.slice(6));
|
||||||
|
handleStreamEvent(eventData, progressElement, progressId,
|
||||||
|
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
||||||
|
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析事件数据失败:', e, line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 处理剩余的buffer
|
||||||
// 处理剩余的buffer
|
if (buffer.trim()) {
|
||||||
if (buffer.trim()) {
|
const lines = buffer.split('\n');
|
||||||
const lines = buffer.split('\n');
|
for (const line of lines) {
|
||||||
for (const line of lines) {
|
if (line.startsWith('data: ')) {
|
||||||
if (line.startsWith('data: ')) {
|
try {
|
||||||
try {
|
const eventData = JSON.parse(line.slice(6));
|
||||||
const eventData = JSON.parse(line.slice(6));
|
handleStreamEvent(eventData, progressElement, progressId,
|
||||||
handleStreamEvent(eventData, progressElement, progressId,
|
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
||||||
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
||||||
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
} catch (e) {
|
||||||
} catch (e) {
|
console.error('解析事件数据失败:', e, line);
|
||||||
console.error('解析事件数据失败:', e, line);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
window.__csAgentLiveStream = { active: false, conversationId: null, progressId: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息发送成功后,再次确保草稿被清除
|
// 消息发送成功后,再次确保草稿被清除
|
||||||
clearChatDraft();
|
clearChatDraft();
|
||||||
try {
|
try {
|
||||||
@@ -2922,6 +2931,22 @@ async function loadConversation(conversationId) {
|
|||||||
await window.restoreHitlInlineForConversation(conversationId);
|
await window.restoreHitlInlineForConversation(conversationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面刷新后主流式连接会中断;若该会话仍在后端运行,自动挂载 task-events 补流继续更新前端迭代进度。
|
||||||
|
const skipReplay = typeof window.shouldSkipTaskEventReplayAttach === 'function'
|
||||||
|
&& window.shouldSkipTaskEventReplayAttach(conversationId);
|
||||||
|
if (
|
||||||
|
seq === loadConversationRequestSeq &&
|
||||||
|
currentConversationId === conversationId &&
|
||||||
|
typeof window.attachRunningTaskEventStream === 'function' &&
|
||||||
|
!skipReplay
|
||||||
|
) {
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => window.attachRunningTaskEventStream(conversationId))
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('attachRunningTaskEventStream on loadConversation failed', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载对话失败:', error);
|
console.error('加载对话失败:', error);
|
||||||
alert('加载对话失败: ' + error.message);
|
alert('加载对话失败: ' + error.message);
|
||||||
|
|||||||
+136
-72
@@ -3,6 +3,36 @@ let activeTaskInterval = null;
|
|||||||
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
|
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
|
||||||
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
|
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主对话 POST 流仍在读取时,禁止再挂 task-events 补流,否则同一事件会画两遍(与 HITL 是否开启无关)。
|
||||||
|
* window.__csAgentLiveStream 由 chat.js sendMessage 在读到 body 后设置,在 finally 中清除。
|
||||||
|
*/
|
||||||
|
function syncAgentLiveStreamConversationId(cid) {
|
||||||
|
if (!cid) return;
|
||||||
|
try {
|
||||||
|
const live = window.__csAgentLiveStream;
|
||||||
|
if (live && live.active) {
|
||||||
|
live.conversationId = cid;
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipTaskEventReplayAttach(conversationId) {
|
||||||
|
try {
|
||||||
|
const live = window.__csAgentLiveStream;
|
||||||
|
if (!live || !live.active || !live.progressId) return false;
|
||||||
|
if (!document.getElementById(live.progressId)) return false;
|
||||||
|
// 新会话:conversation 事件尚未到达前 conversationId 可能仍为 null,一律不补挂
|
||||||
|
if (live.conversationId == null) return true;
|
||||||
|
return live.conversationId === conversationId;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.shouldSkipTaskEventReplayAttach = shouldSkipTaskEventReplayAttach;
|
||||||
|
}
|
||||||
|
|
||||||
// 当前界面语言对应的 BCP 47 标签(与时间格式化一致)
|
// 当前界面语言对应的 BCP 47 标签(与时间格式化一致)
|
||||||
function getCurrentTimeLocale() {
|
function getCurrentTimeLocale() {
|
||||||
if (typeof window.__locale === 'string' && window.__locale.length) {
|
if (typeof window.__locale === 'string' && window.__locale.length) {
|
||||||
@@ -934,6 +964,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
|
|
||||||
// 更新当前对话ID
|
// 更新当前对话ID
|
||||||
currentConversationId = event.data.conversationId;
|
currentConversationId = event.data.conversationId;
|
||||||
|
syncAgentLiveStreamConversationId(event.data.conversationId);
|
||||||
updateActiveConversation();
|
updateActiveConversation();
|
||||||
addAttackChainButton(currentConversationId);
|
addAttackChainButton(currentConversationId);
|
||||||
loadActiveTasks();
|
loadActiveTasks();
|
||||||
@@ -1472,6 +1503,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
currentConversationId = responseData.conversationId;
|
currentConversationId = responseData.conversationId;
|
||||||
|
syncAgentLiveStreamConversationId(responseData.conversationId);
|
||||||
updateActiveConversation();
|
updateActiveConversation();
|
||||||
addAttackChainButton(currentConversationId);
|
addAttackChainButton(currentConversationId);
|
||||||
updateProgressConversation(progressId, responseData.conversationId);
|
updateProgressConversation(progressId, responseData.conversationId);
|
||||||
@@ -1552,6 +1584,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentConversationId = responseData.conversationId;
|
currentConversationId = responseData.conversationId;
|
||||||
|
syncAgentLiveStreamConversationId(responseData.conversationId);
|
||||||
updateActiveConversation();
|
updateActiveConversation();
|
||||||
addAttackChainButton(currentConversationId);
|
addAttackChainButton(currentConversationId);
|
||||||
updateProgressConversation(progressId, responseData.conversationId);
|
updateProgressConversation(progressId, responseData.conversationId);
|
||||||
@@ -1682,6 +1715,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
// 更新对话ID
|
// 更新对话ID
|
||||||
if (event.data && event.data.conversationId) {
|
if (event.data && event.data.conversationId) {
|
||||||
currentConversationId = event.data.conversationId;
|
currentConversationId = event.data.conversationId;
|
||||||
|
syncAgentLiveStreamConversationId(event.data.conversationId);
|
||||||
updateActiveConversation();
|
updateActiveConversation();
|
||||||
addAttackChainButton(currentConversationId);
|
addAttackChainButton(currentConversationId);
|
||||||
updateProgressConversation(progressId, event.data.conversationId);
|
updateProgressConversation(progressId, event.data.conversationId);
|
||||||
@@ -1982,90 +2016,120 @@ async function refreshLastAssistantProcessDetails(conversationId) {
|
|||||||
|
|
||||||
window.refreshLastAssistantProcessDetails = refreshLastAssistantProcessDetails;
|
window.refreshLastAssistantProcessDetails = refreshLastAssistantProcessDetails;
|
||||||
|
|
||||||
|
const taskEventReplayAttachState = {
|
||||||
|
conversationId: null,
|
||||||
|
inFlightPromise: null
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅运行中任务的 SSE 镜像(GET /api/agent-loop/task-events),用于 HITL 通过后主连接已断开时接续 UI。
|
* 订阅运行中任务的 SSE 镜像(GET /api/agent-loop/task-events),用于 HITL 通过后主连接已断开时接续 UI。
|
||||||
*/
|
*/
|
||||||
async function attachRunningTaskEventStream(conversationId) {
|
async function attachRunningTaskEventStream(conversationId) {
|
||||||
if (!conversationId || typeof apiFetch !== 'function') return false;
|
if (!conversationId || typeof apiFetch !== 'function') return false;
|
||||||
try {
|
if (
|
||||||
const check = await apiFetch('/api/agent-loop/tasks');
|
taskEventReplayAttachState.inFlightPromise &&
|
||||||
if (!check.ok) return false;
|
taskEventReplayAttachState.conversationId === conversationId
|
||||||
const j = await check.json().catch(function () { return {}; });
|
) {
|
||||||
const active = (j.tasks || []).some(function (t) {
|
return taskEventReplayAttachState.inFlightPromise;
|
||||||
return t && t.conversationId === conversationId && (t.status === 'running' || t.status === 'cancelling');
|
}
|
||||||
});
|
if (shouldSkipTaskEventReplayAttach(conversationId)) {
|
||||||
if (!active) return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const asEl = findLastAssistantMessageElInChat();
|
const attachPromise = (async function () {
|
||||||
if (!asEl || !asEl.id) return false;
|
try {
|
||||||
const backendId = asEl.dataset && asEl.dataset.backendMessageId;
|
const check = await apiFetch('/api/agent-loop/tasks');
|
||||||
if (backendId && typeof renderProcessDetails === 'function') {
|
if (!check.ok) return false;
|
||||||
const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
|
const j = await check.json().catch(function () { return {}; });
|
||||||
const jd = await res.json().catch(function () { return {}; });
|
const active = (j.tasks || []).some(function (t) {
|
||||||
if (res.ok && Array.isArray(jd.processDetails)) {
|
return t && t.conversationId === conversationId && (t.status === 'running' || t.status === 'cancelling');
|
||||||
renderProcessDetails(asEl.id, jd.processDetails);
|
});
|
||||||
}
|
if (!active) return false;
|
||||||
}
|
|
||||||
expandProcessDetailsTimeline(asEl.id);
|
|
||||||
|
|
||||||
const progressId = taskReplayProgressId(conversationId);
|
const asEl = findLastAssistantMessageElInChat();
|
||||||
beginCsTaskReplay(progressId, asEl.id, conversationId);
|
if (!asEl || !asEl.id) return false;
|
||||||
|
const backendId = asEl.dataset && asEl.dataset.backendMessageId;
|
||||||
const url = '/api/agent-loop/task-events?conversationId=' + encodeURIComponent(conversationId);
|
if (backendId && typeof renderProcessDetails === 'function') {
|
||||||
const response = await apiFetch(url, {
|
const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
|
||||||
method: 'GET',
|
const jd = await res.json().catch(function () { return {}; });
|
||||||
headers: { Accept: 'text/event-stream' }
|
if (res.ok && Array.isArray(jd.processDetails)) {
|
||||||
});
|
renderProcessDetails(asEl.id, jd.processDetails);
|
||||||
if (!response.ok) {
|
// renderProcessDetails 会重建时间线节点,需重新挂载 HITL 审批入口
|
||||||
clearCsTaskReplay();
|
if (typeof window.restoreHitlInlineForConversation === 'function') {
|
||||||
if (progressTaskState.has(progressId)) {
|
await window.restoreHitlInlineForConversation(conversationId);
|
||||||
progressTaskState.delete(progressId);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mcpIds = [];
|
|
||||||
const assistantDomId = asEl.id;
|
|
||||||
const getAssistantIdFn = function () { return assistantDomId; };
|
|
||||||
const setAssistantIdFn = function () {};
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
while (true) {
|
|
||||||
const chunk = await reader.read();
|
|
||||||
if (chunk.done) break;
|
|
||||||
buffer += decoder.decode(chunk.value, { stream: true });
|
|
||||||
const lines = buffer.split('\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
for (let li = 0; li < lines.length; li++) {
|
|
||||||
const line = lines[li];
|
|
||||||
if (line.indexOf('data: ') === 0) {
|
|
||||||
try {
|
|
||||||
const eventData = JSON.parse(line.slice(6));
|
|
||||||
handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = ids; });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('task-events parse', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
expandProcessDetailsTimeline(asEl.id);
|
||||||
if (window.csTaskReplay && window.csTaskReplay.progressId === progressId) {
|
|
||||||
|
const progressId = taskReplayProgressId(conversationId);
|
||||||
|
beginCsTaskReplay(progressId, asEl.id, conversationId);
|
||||||
|
|
||||||
|
const url = '/api/agent-loop/task-events?conversationId=' + encodeURIComponent(conversationId);
|
||||||
|
const response = await apiFetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'text/event-stream' }
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
clearCsTaskReplay();
|
||||||
|
if (progressTaskState.has(progressId)) {
|
||||||
|
progressTaskState.delete(progressId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mcpIds = [];
|
||||||
|
const assistantDomId = asEl.id;
|
||||||
|
const getAssistantIdFn = function () { return assistantDomId; };
|
||||||
|
const setAssistantIdFn = function () {};
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
while (true) {
|
||||||
|
const chunk = await reader.read();
|
||||||
|
if (chunk.done) break;
|
||||||
|
buffer += decoder.decode(chunk.value, { stream: true });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
for (let li = 0; li < lines.length; li++) {
|
||||||
|
const line = lines[li];
|
||||||
|
if (line.indexOf('data: ') === 0) {
|
||||||
|
try {
|
||||||
|
const eventData = JSON.parse(line.slice(6));
|
||||||
|
handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = ids; });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('task-events parse', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (window.csTaskReplay && window.csTaskReplay.progressId === progressId) {
|
||||||
|
clearCsTaskReplay();
|
||||||
|
}
|
||||||
|
if (progressTaskState.has(progressId)) {
|
||||||
|
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成');
|
||||||
|
}
|
||||||
|
if (typeof loadActiveTasks === 'function') loadActiveTasks();
|
||||||
|
if (typeof window.loadConversation === 'function' && window.currentConversationId === conversationId) {
|
||||||
|
await window.loadConversation(conversationId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('attachRunningTaskEventStream', e);
|
||||||
clearCsTaskReplay();
|
clearCsTaskReplay();
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (taskEventReplayAttachState.inFlightPromise === attachPromise) {
|
||||||
|
taskEventReplayAttachState.inFlightPromise = null;
|
||||||
|
taskEventReplayAttachState.conversationId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (progressTaskState.has(progressId)) {
|
})();
|
||||||
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成');
|
|
||||||
}
|
taskEventReplayAttachState.conversationId = conversationId;
|
||||||
if (typeof loadActiveTasks === 'function') loadActiveTasks();
|
taskEventReplayAttachState.inFlightPromise = attachPromise;
|
||||||
if (typeof window.loadConversation === 'function' && window.currentConversationId === conversationId) {
|
return attachPromise;
|
||||||
await window.loadConversation(conversationId);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('attachRunningTaskEventStream', e);
|
|
||||||
clearCsTaskReplay();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.attachRunningTaskEventStream = attachRunningTaskEventStream;
|
window.attachRunningTaskEventStream = attachRunningTaskEventStream;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 442 KiB After Width: | Height: | Size: 85 KiB |
Reference in New Issue
Block a user