Compare commits

...

17 Commits

Author SHA1 Message Date
公明 55b6bceb21 Update config.yaml 2026-04-26 15:11:48 +08:00
公明 65d73b3d66 Add files via upload 2026-04-26 15:08:48 +08:00
公明 913115d1fb Add files via upload 2026-04-26 04:26:29 +08:00
公明 e1b967d781 Add files via upload 2026-04-26 04:18:38 +08:00
公明 9d9efa886f Add files via upload 2026-04-26 04:17:27 +08:00
公明 cae45e9dc5 Add files via upload 2026-04-26 04:16:25 +08:00
公明 c788b59f25 Update config.yaml 2026-04-24 20:01:42 +08:00
公明 5edf3a70f9 Add files via upload 2026-04-24 20:00:50 +08:00
公明 3dfb3b4e82 Add files via upload 2026-04-24 19:59:15 +08:00
公明 a517fe0931 Add files via upload 2026-04-24 19:56:09 +08:00
公明 0ab5e31a64 Add files via upload 2026-04-24 18:24:52 +08:00
公明 ea6e027b25 Add files via upload 2026-04-24 17:30:22 +08:00
公明 ba9d2f0afd Update config.yaml 2026-04-24 15:43:00 +08:00
公明 6ce835703e Add files via upload 2026-04-24 11:24:10 +08:00
公明 666980ad8f Add files via upload 2026-04-24 11:08:47 +08:00
公明 bc8e81307e Add files via upload 2026-04-24 11:07:03 +08:00
公明 053534feaa Add files via upload 2026-04-24 11:04:55 +08:00
34 changed files with 721 additions and 485 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<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>
# CyberStrikeAI
+1 -1
View File
@@ -1,5 +1,5 @@
<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>
# CyberStrikeAI
+1 -1
View File
@@ -10,7 +10,7 @@
# ============================================
# 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.5.5"
version: "v1.5.8"
# 服务器配置
server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+1
View File
@@ -657,6 +657,7 @@ func setupRoutes(
protected.POST("/eino-agent/stream", agentHandler.EinoSingleAgentLoopStream)
protected.GET("/hitl/pending", agentHandler.ListHITLPending)
protected.POST("/hitl/decision", agentHandler.DecideHITLInterrupt)
protected.POST("/hitl/dismiss", agentHandler.DismissHITLInterrupt)
protected.GET("/hitl/config/:conversationId", agentHandler.GetHITLConversationConfig)
protected.PUT("/hitl/config", agentHandler.UpsertHITLConversationConfig)
protected.POST("/hitl/tool-whitelist", agentHandler.MergeHITLGlobalToolWhitelist)
+8 -3
View File
@@ -603,11 +603,13 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
baseCtx, cancelWithCause := context.WithCancelCause(c.Request.Context())
defer cancelWithCause(nil)
progressCallback := h.createProgressCallback(baseCtx, cancelWithCause, conversationID, "", nil)
baseCtx = h.injectReactHITLInterceptor(baseCtx, cancelWithCause, conversationID, "", nil)
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
defer timeoutCancel()
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, conversationID, "", nil)
taskCtx = h.injectReactHITLInterceptor(taskCtx, cancelWithCause, conversationID, "", nil)
// 执行Agent Loop,传入历史消息和对话ID(使用包含角色提示词的finalMessage和角色工具列表)
result, err := h.agent.AgentLoopWithProgress(baseCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
@@ -1209,6 +1211,9 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
}
eventJSON, _ := json.Marshal(event)
fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON)
done := StreamEvent{Type: "done", Message: ""}
doneJSON, _ := json.Marshal(done)
fmt.Fprintf(c.Writer, "data: %s\n\n", doneJSON)
c.Writer.Flush()
return
}
+5 -3
View File
@@ -281,8 +281,10 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
}
baseCtx, cancelWithCause := context.WithCancelCause(c.Request.Context())
defer cancelWithCause(nil)
progressCallback := h.createProgressCallback(baseCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, progressCallbackRaw)
baseCtx = multiagent.WithHITLToolInterceptor(baseCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
defer timeoutCancel()
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, progressCallbackRaw)
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
return h.interceptHITLForEinoTool(ctx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil, toolName, arguments)
})
@@ -292,7 +294,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
}
result, runErr := multiagent.RunEinoSingleChatModelAgent(
baseCtx,
taskCtx,
h.config,
&h.config.MultiAgent,
h.agent,
+51 -1
View File
@@ -88,7 +88,20 @@ CREATE TABLE IF NOT EXISTS hitl_conversation_configs (
timeout_seconds INTEGER NOT NULL DEFAULT 300,
updated_at DATETIME NOT NULL
);`)
return err
if err != nil {
return err
}
// On startup, cancel all orphaned pending interrupts from previous process.
// Their in-memory channels are gone, so they can never be resolved.
res, err := m.db.Exec(`UPDATE hitl_interrupts SET status='cancelled', decision='reject',
decision_comment='process restarted', decided_at=CURRENT_TIMESTAMP WHERE status='pending'`)
if err != nil {
m.logger.Warn("failed to cancel orphaned HITL interrupts", zap.Error(err))
} else if n, _ := res.RowsAffected(); n > 0 {
m.logger.Info("cancelled orphaned HITL interrupts from previous process", zap.Int64("count", n))
}
return nil
}
func normalizeHitlMode(mode string) string {
@@ -587,6 +600,43 @@ func (h *AgentHandler) DecideHITLInterrupt(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *AgentHandler) DismissHITLInterrupt(c *gin.Context) {
var req struct {
InterruptID string `json:"interruptId" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if h.hitlManager == nil {
c.JSON(500, gin.H{"error": "hitl manager unavailable"})
return
}
res, err := h.db.Exec(`UPDATE hitl_interrupts SET status='cancelled', decision='reject',
decision_comment='dismissed by user', decided_at=CURRENT_TIMESTAMP
WHERE id=? AND status='pending'`, req.InterruptID)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
n, _ := res.RowsAffected()
if n == 0 {
c.JSON(404, gin.H{"error": "interrupt not found or already resolved"})
return
}
// Also drain from in-memory map if present
h.hitlManager.mu.Lock()
if p, ok := h.hitlManager.pending[req.InterruptID]; ok {
delete(h.hitlManager.pending, req.InterruptID)
select {
case p.decideCh <- hitlDecision{Decision: "reject", Comment: "dismissed by user"}:
default:
}
}
h.hitlManager.mu.Unlock()
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *AgentHandler) interceptHITLForEinoTool(runCtx context.Context, cancelRun context.CancelCauseFunc, conversationID, assistantMessageID string, sendEventFunc func(eventType, message string, data interface{}), toolName, arguments string) (string, error) {
payload := map[string]interface{}{
"toolName": toolName,
+8 -3
View File
@@ -40,6 +40,9 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
event := StreamEvent{Type: "error", Message: "请求参数错误: " + err.Error()}
b, _ := json.Marshal(event)
fmt.Fprintf(c.Writer, "data: %s\n\n", b)
done := StreamEvent{Type: "done", Message: ""}
db, _ := json.Marshal(done)
fmt.Fprintf(c.Writer, "data: %s\n\n", db)
c.Writer.Flush()
return
}
@@ -293,13 +296,15 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
baseCtx, cancelWithCause := context.WithCancelCause(c.Request.Context())
defer cancelWithCause(nil)
progressCallback := h.createProgressCallback(baseCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil)
baseCtx = multiagent.WithHITLToolInterceptor(baseCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
defer timeoutCancel()
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil)
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
return h.interceptHITLForEinoTool(ctx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil, toolName, arguments)
})
result, runErr := multiagent.RunDeepAgent(
baseCtx,
taskCtx,
h.config,
&h.config.MultiAgent,
h.agent,
+140 -65
View File
@@ -18,6 +18,21 @@ import (
"go.uber.org/zap"
)
func isEinoIterationLimitError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(strings.TrimSpace(err.Error()))
if msg == "" {
return false
}
return strings.Contains(msg, "max iteration") ||
strings.Contains(msg, "maximum iteration") ||
strings.Contains(msg, "maximum iterations") ||
strings.Contains(msg, "iteration limit") ||
strings.Contains(msg, "达到最大迭代")
}
// einoADKRunLoopArgs 将 Eino adk.Runner 事件循环从 RunDeepAgent / RunEinoSingleChatModelAgent 中抽出复用。
type einoADKRunLoopArgs struct {
OrchMode string
@@ -205,6 +220,98 @@ attemptLoop:
}
runner := adk.NewRunner(ctx, runnerCfg)
iter := runner.Run(ctx, msgs)
handleRunErr := func(runErr error, attempt int, reasonOverride string) (retry bool, retErr error) {
if runErr == nil {
return false, nil
}
if errors.Is(runErr, context.DeadlineExceeded) {
flushAllPendingAsFailed(runErr)
if progress != nil {
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"errorKind": "timeout",
})
}
return false, runErr
}
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
if errors.Is(runErr, context.Canceled) {
flushAllPendingAsFailed(runErr)
if progress != nil {
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return false, runErr
}
if isEinoIterationLimitError(runErr) {
flushAllPendingAsFailed(runErr)
if progress != nil {
progress("iteration_limit_reached", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"orchestration": orchMode,
})
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"errorKind": "iteration_limit",
})
}
return false, runErr
}
canRetry := attempt+1 < maxToolCallRecoveryAttempts
if !canRetry {
// 重试次数已耗尽,终止。
flushAllPendingAsFailed(runErr)
if progress != nil {
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return false, runErr
}
// 区分错误类型以选择最合适的纠错提示,但无论哪种都执行重试(default-soft)。
var hint *schema.Message
var reason, timelineMsg string
switch {
case strings.TrimSpace(reasonOverride) != "":
hint = toolExecutionRetryHint()
reason = strings.TrimSpace(reasonOverride)
timelineMsg = toolExecutionRecoveryTimelineMessage(attempt)
case isRecoverableToolCallArgumentsJSONError(runErr):
hint = toolCallArgumentsJSONRetryHint()
reason = "invalid_tool_arguments_json"
timelineMsg = toolCallArgumentsJSONRecoveryTimelineMessage(attempt)
default:
hint = toolExecutionRetryHint()
reason = "tool_execution_error"
timelineMsg = toolExecutionRecoveryTimelineMessage(attempt)
}
if logger != nil {
logger.Warn("eino: recoverable error, will retry with corrective hint",
zap.Error(runErr), zap.Int("attempt", attempt), zap.String("reason", reason))
}
flushAllPendingAsFailed(runErr)
retryHints = append(retryHints, hint)
if progress != nil {
progress("eino_recovery", timelineMsg, map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"einoRetry": attempt,
"runIndex": attempt + 1,
"maxRuns": maxToolCallRecoveryAttempts,
"reason": reason,
})
}
return true, nil
}
for {
// 检测 context 取消(用户关闭浏览器、请求超时等),flush pending 工具状态避免 UI 卡在 "执行中"。
@@ -223,6 +330,18 @@ attemptLoop:
ev, ok := iter.Next()
if !ok {
if len(pendingByID) > 0 {
orphanCount := len(pendingByID)
flushAllPendingAsFailed(errors.New("pending tool call missing result before run completion"))
if progress != nil {
progress("eino_pending_orphaned", "pending tool calls were force-closed at run end", map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"orchestration": orchMode,
"pendingCount": orphanCount,
})
}
}
lastRunMsgs = msgs
break attemptLoop
}
@@ -230,72 +349,11 @@ attemptLoop:
continue
}
if ev.Err != nil {
if errors.Is(ev.Err, context.DeadlineExceeded) {
flushAllPendingAsFailed(ev.Err)
if progress != nil {
progress("error", ev.Err.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"errorKind": "timeout",
})
}
return nil, ev.Err
if retry, retErr := handleRunErr(ev.Err, attempt, ""); retErr != nil {
return nil, retErr
} else if retry {
continue attemptLoop
}
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
if errors.Is(ev.Err, context.Canceled) {
flushAllPendingAsFailed(ev.Err)
if progress != nil {
progress("error", ev.Err.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return nil, ev.Err
}
canRetry := attempt+1 < maxToolCallRecoveryAttempts
if !canRetry {
// 重试次数已耗尽,终止。
flushAllPendingAsFailed(ev.Err)
if progress != nil {
progress("error", ev.Err.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return nil, ev.Err
}
// 区分错误类型以选择最合适的纠错提示,但无论哪种都执行重试(default-soft)。
var hint *schema.Message
var reason, timelineMsg string
if isRecoverableToolCallArgumentsJSONError(ev.Err) {
hint = toolCallArgumentsJSONRetryHint()
reason = "invalid_tool_arguments_json"
timelineMsg = toolCallArgumentsJSONRecoveryTimelineMessage(attempt)
} else {
hint = toolExecutionRetryHint()
reason = "tool_execution_error"
timelineMsg = toolExecutionRecoveryTimelineMessage(attempt)
}
if logger != nil {
logger.Warn("eino: recoverable error, will retry with corrective hint",
zap.Error(ev.Err), zap.Int("attempt", attempt), zap.String("reason", reason))
}
flushAllPendingAsFailed(ev.Err)
retryHints = append(retryHints, hint)
if progress != nil {
progress("eino_recovery", timelineMsg, map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"einoRetry": attempt,
"runIndex": attempt + 1,
"maxRuns": maxToolCallRecoveryAttempts,
"reason": reason,
})
}
continue attemptLoop
}
if ev.AgentName != "" && progress != nil {
iterEinoAgent := orchestratorName
@@ -349,6 +407,7 @@ attemptLoop:
var subAssistantBuf strings.Builder
var subReplyStreamID string
var mainAssistantBuf strings.Builder
var streamRecvErr error
for {
chunk, rerr := mv.MessageStream.Recv()
if rerr != nil {
@@ -361,6 +420,7 @@ attemptLoop:
zap.String("agent", ev.AgentName),
zap.Int("toolFragments", len(toolStreamFragments)))
}
streamRecvErr = rerr
break
}
if chunk == nil {
@@ -459,6 +519,21 @@ attemptLoop:
lastToolChunk = &schema.Message{ToolCalls: merged}
}
tryEmitToolCallsOnce(lastToolChunk, ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
if streamRecvErr != nil {
if progress != nil {
progress("eino_stream_error", streamRecvErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
})
}
if retry, retErr := handleRunErr(streamRecvErr, attempt, "stream_recv_error"); retErr != nil {
return nil, retErr
} else if retry {
continue attemptLoop
}
}
continue
}
@@ -64,7 +64,7 @@ public class BurpExtender implements IBurpExtender, IContextMenuFactory {
String prompt = HttpMessageFormatter.toPrompt(helpers, msg, instruction);
String title = HttpMessageFormatter.getRequestTitle(helpers, msg);
String agentModeStr = (cfg.agentMode == CyberStrikeAIClient.AgentMode.MULTI) ? "Multi Agent" : "Single Agent";
String agentModeStr = cfg.agentMode.displayName;
String runId = tab.startNewRun(title, agentModeStr, msg);
tab.appendProgressToRun(runId, "\n[server] " + cfg.baseUrl + "\n\n");
@@ -26,8 +26,21 @@ final class CyberStrikeAIClient {
}
enum AgentMode {
SINGLE,
MULTI
NATIVE_REACT("Native ReAct", "/api/agent-loop/stream", null),
EINO_SINGLE("Eino Single (ADK)", "/api/eino-agent/stream", null),
DEEP("Deep (DeepAgent)", "/api/multi-agent/stream", "deep"),
PLAN_EXECUTE("Plan-Execute", "/api/multi-agent/stream", "plan_execute"),
SUPERVISOR("Supervisor", "/api/multi-agent/stream", "supervisor");
final String displayName;
final String streamPath;
final String orchestration;
AgentMode(String displayName, String streamPath, String orchestration) {
this.displayName = displayName;
this.streamPath = streamPath;
this.orchestration = orchestration;
}
}
interface StreamListener {
@@ -94,13 +107,15 @@ final class CyberStrikeAIClient {
}
void streamTest(Config cfg, String token, String message, StreamListener listener) {
String path = (cfg.agentMode == AgentMode.MULTI) ? "/api/multi-agent/stream" : "/api/agent-loop/stream";
String urlStr = cfg.baseUrl + path;
String urlStr = cfg.baseUrl + cfg.agentMode.streamPath;
Map<String, Object> payload = new HashMap<>();
payload.put("message", message);
payload.put("conversationId", "");
payload.put("role", "");
if (cfg.agentMode.orchestration != null) {
payload.put("orchestration", cfg.agentMode.orchestration);
}
new Thread(() -> {
HttpURLConnection conn = null;
@@ -184,11 +199,16 @@ final class CyberStrikeAIClient {
String message = payload.get("message") != null ? String.valueOf(payload.get("message")) : "";
String conversationId = payload.get("conversationId") != null ? String.valueOf(payload.get("conversationId")) : "";
String role = payload.get("role") != null ? String.valueOf(payload.get("role")) : "";
return "{"
+ "\"message\":\"" + escapeJson(message) + "\","
+ "\"conversationId\":\"" + escapeJson(conversationId) + "\","
+ "\"role\":\"" + escapeJson(role) + "\""
+ "}";
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"message\":\"").append(escapeJson(message)).append("\",");
sb.append("\"conversationId\":\"").append(escapeJson(conversationId)).append("\",");
sb.append("\"role\":\"").append(escapeJson(role)).append("\"");
if (payload.containsKey("orchestration") && payload.get("orchestration") != null) {
sb.append(",\"orchestration\":\"").append(escapeJson(String.valueOf(payload.get("orchestration")))).append("\"");
}
sb.append("}");
return sb.toString();
}
private static String escapeJson(String s) {
@@ -15,7 +15,9 @@ final class CyberStrikeAITab implements ITab {
private final JTextField hostField = new JTextField("127.0.0.1");
private final JTextField portField = new JTextField("8080");
private final JPasswordField passwordField = new JPasswordField();
private final JComboBox<String> agentModeBox = new JComboBox<>(new String[]{"Single Agent", "Multi Agent"});
private final JComboBox<String> agentModeBox = new JComboBox<>(new String[]{
"Native ReAct", "Eino Single (ADK)", "Deep (DeepAgent)", "Plan-Execute", "Supervisor"
});
private final JButton validateButton = new JButton("Validate");
private final JButton clearButton = new JButton("Clear Output");
private final JButton stopButton = new JButton("Stop");
@@ -98,7 +100,7 @@ final class CyberStrikeAITab implements ITab {
hostField.setColumns(14);
portField.setColumns(6);
passwordField.setColumns(12);
agentModeBox.setPreferredSize(new Dimension(160, agentModeBox.getPreferredSize().height));
agentModeBox.setPreferredSize(new Dimension(200, agentModeBox.getPreferredSize().height));
JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 2));
row1.add(new JLabel("Host"));
@@ -475,14 +477,17 @@ final class CyberStrikeAITab implements ITab {
renderMarkdownBox.addActionListener(e -> refreshOutputView());
}
private static final CyberStrikeAIClient.AgentMode[] AGENT_MODES = CyberStrikeAIClient.AgentMode.values();
CyberStrikeAIClient.Config currentConfig() {
String host = hostField.getText().trim();
String port = portField.getText().trim();
String password = new String(passwordField.getPassword());
String baseUrl = "http://" + host + ":" + port;
CyberStrikeAIClient.AgentMode mode = agentModeBox.getSelectedIndex() == 1
? CyberStrikeAIClient.AgentMode.MULTI
: CyberStrikeAIClient.AgentMode.SINGLE;
int idx = agentModeBox.getSelectedIndex();
CyberStrikeAIClient.AgentMode mode = (idx >= 0 && idx < AGENT_MODES.length)
? AGENT_MODES[idx]
: CyberStrikeAIClient.AgentMode.NATIVE_REACT;
return new CyberStrikeAIClient.Config(baseUrl, password, mode);
}
-15
View File
@@ -2,19 +2,4 @@ name: API安全测试
description: API安全测试专家,专注于API接口安全检测
user_prompt: 你是一个专业的API安全测试专家。请使用专业的API测试工具对目标API接口进行全面的安全检测,包括GraphQL安全、API参数fuzzing、JWT分析、API架构分析等工作。
icon: "\U0001F4E1"
tools:
- api-fuzzer
- api-schema-analyzer
- graphql-scanner
- arjun
- jwt-analyzer
- http-intruder
- http-framework-test
- burpsuite
- httpx
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-28
View File
@@ -2,32 +2,4 @@ name: CTF
description: CTF竞赛专家,擅长解题和漏洞利用
user_prompt: 你是一个CTF竞赛专家。请使用CTF解题思维和方法,快速定位和利用漏洞,解决各类CTF题目。
icon: "\U0001F3C6"
tools:
- amass
- anew
- angr
- api-fuzzer
- api-schema-analyzer
- arjun
- arp-scan
- autorecon
- binwalk
- bloodhound
- burpsuite
- cat
- checkov
- checksec
- cloudmapper
- create-file
- cyberchef
- dalfox
- delete-file
- httpx
- http-framework-test
- exec
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-20
View File
@@ -2,24 +2,4 @@ name: Web应用扫描
description: Web应用漏洞扫描专家,全面的Web安全检测
user_prompt: 你是一个专业的Web应用漏洞扫描专家。请使用各种Web扫描工具对目标Web应用进行全面的安全检测,包括目录枚举、文件扫描、漏洞识别等工作。
icon: "\U0001F310"
tools:
- dirsearch
- dirb
- gobuster
- feroxbuster
- ffuf
- wfuzz
- sqlmap
- dalfox
- xsser
- nikto
- nuclei
- wpscan
- httpx
- http-framework-test
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-14
View File
@@ -2,18 +2,4 @@ name: Web框架测试
description: Web框架安全测试专家,专注于Web应用框架漏洞检测
user_prompt: 你是一个专业的Web框架安全测试专家。请使用专业的工具对Web应用框架进行安全测试,识别框架相关的安全漏洞和配置问题。
icon: "\U0001F310"
tools:
- http-framework-test
- nikto
- nuclei
- wafw00f
- wpscan
- httpx
- burpsuite
- zap
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-26
View File
@@ -2,30 +2,4 @@ name: 二进制分析
description: 二进制分析与利用专家,擅长逆向工程和密码破解
user_prompt: 你是一个专业的二进制分析与利用专家。请使用逆向工程工具分析二进制文件,识别漏洞,进行利用开发。同时擅长密码破解、哈希分析等技术。
icon: "\U0001F52C"
tools:
- dirsearch
- docker-bench-security
- exec
- execute-python-script
- install-python-package
- ghidra
- graphql-scanner
- hakrawler
- hash-identifier
- hashcat
- hashpump
- http-framework-test
- httpx
- gdb
- radare2
- objdump
- strings
- binwalk
- ropper
- ropgadget
- john
- cyberchef
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-12
View File
@@ -2,16 +2,4 @@ name: 云安全审计
description: 云安全审计专家,多云环境安全检测
user_prompt: 你是一个专业的云安全审计专家。请使用专业的云安全工具对AWS、Azure、GCP等云环境进行全面的安全审计,包括配置检查、合规性评估、权限审计、安全最佳实践验证等工作。
icon:
tools:
- prowler
- scout-suite
- cloudmapper
- pacu
- terrascan
- checkov
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-26
View File
@@ -2,30 +2,4 @@ name: 信息收集
description: 资产发现与信息搜集专家
user_prompt: 你是一个专业的信息收集专家。请使用各种信息收集技术和工具,对目标进行全面的资产发现、子域名枚举、端口扫描、服务识别等信息收集工作。
icon: "\U0001F50D"
tools:
- amass
- subfinder
- dnsenum
- fierce
- fofa_search
- zoomeye_search
- nmap
- masscan
- rustscan
- arp-scan
- nbtscan
- httpx
- http-framework-test
- katana
- hakrawler
- waybackurls
- paramspider
- gau
- uro
- qsreplace
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-18
View File
@@ -2,22 +2,4 @@ name: 后渗透测试
description: 后渗透测试专家,权限维持与横向移动
user_prompt: 你是一个专业的后渗透测试专家。请使用专业的后渗透工具在获得初始访问权限后进行权限提升、横向移动、权限维持、数据收集等后渗透测试工作。
icon: "\U0001F575"
tools:
- linpeas
- winpeas
- mimikatz
- bloodhound
- impacket
- responder
- netexec
- rpcclient
- smbmap
- enum4linux
- enum4linux-ng
- exec
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-13
View File
@@ -2,17 +2,4 @@ name: 容器安全
description: 容器与Kubernetes安全专家,容器环境安全检测
user_prompt: 你是一个专业的容器与Kubernetes安全专家。请使用专业的容器安全工具对Docker容器和Kubernetes集群进行全面的安全检测,包括镜像漏洞扫描、配置检查、运行时安全等工作。
icon: "\U0001F6E1"
tools:
- trivy
- clair
- docker-bench-security
- kube-bench
- kube-hunter
- falco
- exec
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-19
View File
@@ -2,23 +2,4 @@ name: 数字取证
description: 数字取证与隐写分析专家,文件与内存取证
user_prompt: 你是一个专业的数字取证与隐写分析专家。请使用专业的取证工具对文件、磁盘镜像、内存转储进行分析,提取证据信息。同时擅长隐写分析、数据恢复、元数据提取等技术。
icon: "\U0001F50E"
tools:
- volatility
- volatility3
- foremost
- steghide
- stegsolve
- zsteg
- exiftool
- binwalk
- strings
- xxd
- fcrackzip
- pdfcrack
- exec
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-28
View File
@@ -2,32 +2,4 @@ name: 渗透测试
description: 专业渗透测试专家,全面深入的漏洞检测
user_prompt: 你是一个专业的网络安全渗透测试专家。请使用专业的渗透测试方法和工具,对目标进行全面的安全测试,包括但不限于SQL注入、XSS、CSRF、文件包含、命令执行等常见漏洞。
icon: "\U0001F3AF"
tools:
- http-framework-test
- httpx
- amass
- anew
- angr
- api-fuzzer
- api-schema-analyzer
- arjun
- arp-scan
- autorecon
- binwalk
- bloodhound
- burpsuite
- cat
- checkov
- checksec
- cloudmapper
- create-file
- cyberchef
- dalfox
- delete-file
- exec
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
-18
View File
@@ -2,22 +2,4 @@ name: 综合漏洞扫描
description: 综合漏洞扫描专家,多类型漏洞检测
user_prompt: 你是一个专业的综合漏洞扫描专家。请使用各种漏洞扫描工具对目标进行全面的安全检测,包括Web漏洞、网络服务漏洞、配置缺陷等多种类型的漏洞识别和分析。
icon:
tools:
- nuclei
- nikto
- sqlmap
- nmap
- masscan
- rustscan
- wafw00f
- dalfox
- xsser
- jaeles
- httpx
- http-framework-test
- execute-python-script
- install-python-package
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
enabled: true
+132 -10
View File
@@ -964,7 +964,33 @@ header {
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
cursor: pointer;
user-select: none;
}
.hitl-sidebar-header-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.hitl-sidebar-body {
overflow: hidden;
max-height: 500px;
opacity: 1;
margin-top: 10px;
transition: max-height 0.3s ease, opacity 0.2s ease, margin-top 0.3s ease;
}
.hitl-sidebar-collapsed .hitl-sidebar-body {
max-height: 0;
opacity: 0;
margin-top: 0;
}
.hitl-sidebar-collapsed .hitl-apply-btn {
display: none;
}
.hitl-sidebar-heading {
@@ -1144,12 +1170,18 @@ header {
.hitl-config-input {
width: 100%;
height: 38px;
border: 1px solid var(--border-color);
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
padding: 0 10px;
padding: 0 12px;
font-size: 14px;
color: var(--text-primary);
transition: border-color 0.15s, box-shadow 0.15s;
}
.hitl-config-input:focus {
outline: none;
border-color: var(--accent-color, #0066ff);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.hitl-pending-list {
@@ -1159,43 +1191,133 @@ header {
}
.hitl-pending-item {
border: 1px solid rgba(99, 102, 241, 0.25);
border: 1px solid #dbeafe;
border-radius: 10px;
padding: 12px;
background: rgba(15, 23, 42, 0.45);
padding: 16px;
background: #f8fbff;
transition: box-shadow 0.2s;
}
.hitl-pending-item:hover {
box-shadow: 0 2px 12px rgba(0, 102, 255, 0.08);
}
.hitl-pending-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.hitl-pending-item-title {
display: flex;
align-items: center;
gap: 8px;
}
.hitl-tool-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
background: var(--accent-color, #0066ff);
color: #fff;
}
.hitl-mode-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
background: #e0e7ff;
color: #4338ca;
}
.hitl-mode-tag--review_edit {
background: #fef3c7;
color: #92400e;
}
.hitl-pending-meta {
font-size: 12px;
color: #94a3b8;
margin-bottom: 8px;
word-break: break-all;
}
.hitl-pending-payload {
white-space: pre-wrap;
word-break: break-all;
max-height: 160px;
overflow: auto;
margin: 0 0 4px 0;
padding: 10px 12px;
border-radius: 8px;
background: #fff;
border: 1px solid #e2e8f0;
font-size: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
color: #334155;
line-height: 1.5;
}
.hitl-pending-item-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.hitl-dismiss-btn {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
line-height: 1;
color: var(--text-secondary, #64748b);
padding: 2px 6px;
border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
.hitl-dismiss-btn:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.hitl-pending-actions {
display: flex;
gap: 8px;
margin-top: 10px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e2e8f0;
}
.hitl-edit-args {
width: 100%;
min-height: 76px;
margin-top: 8px;
border: 1px solid rgba(148, 163, 184, 0.35);
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #ffffff;
color: #1f2937;
padding: 8px;
padding: 10px 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.hitl-edit-args:focus {
outline: none;
border-color: var(--accent-color, #0066ff);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.hitl-input-help {
margin-top: 6px;
margin-bottom: 6px;
font-size: 12px;
color: #64748b;
color: var(--accent-color, #0066ff);
line-height: 1.4;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 85 KiB

+7 -1
View File
@@ -179,6 +179,12 @@
"unknownTool": "Unknown tool",
"einoAgentReplyTitle": "Sub-agent reply",
"einoRecoveryTitle": "🔄 Invalid tool JSON · run {{n}}/{{max}} (hint appended)",
"einoStreamErrorTitle": "⚠️ Eino stream interrupted ({{agent}})",
"einoStreamErrorMessage": "Streaming read failed; the system will retry or terminate according to policy.",
"iterationLimitReachedTitle": "⛔ Iteration limit reached",
"iterationLimitReachedMessage": "Maximum iteration count reached; automatic iteration has stopped.",
"einoPendingOrphanedTitle": "🧹 Tool call reconciliation",
"einoPendingOrphanedMessage": "Detected {{count}} unclosed tool call(s); marked as failed and finalized automatically.",
"noDescription": "No description",
"noResponseData": "No response data",
"loading": "Loading...",
@@ -234,7 +240,7 @@
},
"hitl": {
"pageTitle": "HITL approvals",
"pendingTitle": "Pending interrupts"
"pendingTitle": "Pending approvals"
},
"progress": {
"callingAI": "Calling AI model...",
+7 -1
View File
@@ -179,6 +179,12 @@
"unknownTool": "未知工具",
"einoAgentReplyTitle": "子代理回复",
"einoRecoveryTitle": "🔄 工具参数无效 · 第 {{n}}/{{max}} 轮(已追加提示)",
"einoStreamErrorTitle": "⚠️ Eino 流式中断({{agent}}",
"einoStreamErrorMessage": "流式读取异常,系统将按策略重试或结束。",
"iterationLimitReachedTitle": "⛔ 达到迭代上限",
"iterationLimitReachedMessage": "已达到最大迭代次数,任务已停止继续自动迭代。",
"einoPendingOrphanedTitle": "🧹 工具调用收尾补偿",
"einoPendingOrphanedMessage": "检测到 {{count}} 个未闭合工具调用,已自动标记为失败并收尾。",
"noDescription": "暂无描述",
"noResponseData": "暂无响应数据",
"loading": "加载中...",
@@ -234,7 +240,7 @@
},
"hitl": {
"pageTitle": "人机协同审批",
"pendingTitle": "待处理中断"
"pendingTitle": "待处理审批"
},
"progress": {
"callingAI": "正在调用AI模型...",
+79 -37
View File
@@ -399,6 +399,23 @@ if (typeof window !== 'undefined') {
window.updateHitlStatusUI = updateHitlStatusUI;
}
function toggleHitlSidebarCard() {
var card = document.getElementById('hitl-sidebar-card');
if (!card) return;
card.classList.toggle('hitl-sidebar-collapsed');
try {
localStorage.setItem('hitl-sidebar-collapsed', card.classList.contains('hitl-sidebar-collapsed') ? '1' : '0');
} catch (e) {}
}
window.toggleHitlSidebarCard = toggleHitlSidebarCard;
document.addEventListener('DOMContentLoaded', function () {
var card = document.getElementById('hitl-sidebar-card');
if (card && localStorage.getItem('hitl-sidebar-collapsed') === '0') {
card.classList.remove('hitl-sidebar-collapsed');
}
});
function getAgentModeLabelForValue(mode) {
if (typeof window.t === 'function') {
switch (mode) {
@@ -748,50 +765,59 @@ async function sendMessage() {
if (!response.ok) {
throw new Error('请求失败: ' + response.status);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留最后一个不完整的行
for (const line of lines) {
if (line.startsWith('data: ')) {
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);
window.__csAgentLiveStream = {
active: true,
conversationId: currentConversationId || null,
progressId: progressId
};
try {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留最后一个不完整的行
for (const line of lines) {
if (line.startsWith('data: ')) {
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
if (buffer.trim()) {
const lines = buffer.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
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
if (buffer.trim()) {
const lines = buffer.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
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);
}
}
}
}
} finally {
window.__csAgentLiveStream = { active: false, conversationId: null, progressId: null };
}
// 消息发送成功后,再次确保草稿被清除
clearChatDraft();
try {
@@ -2905,6 +2931,22 @@ async function loadConversation(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) {
console.error('加载对话失败:', error);
alert('加载对话失败: ' + error.message);
+34 -10
View File
@@ -282,21 +282,29 @@ async function refreshHitlPending() {
const preview = payload.length > 280 ? (payload.slice(0, 280) + '...') : payload;
const mode = String(item.mode || '').trim().toLowerCase();
const allowEdit = mode === 'review_edit';
var escId = escapeHtml(String(item.id || ''));
var qId = JSON.stringify(String(item.id || '')).replace(/"/g, '&quot;');
var qConv = JSON.stringify(String(item.conversationId || '')).replace(/"/g, '&quot;');
return (
'<div class="hitl-pending-item">' +
'<div class="hitl-pending-item-header">' +
'<strong>' + escapeHtml(item.toolName || '-') + '</strong>' +
'<span>' + escapeHtml(item.mode || '-') + '</span>' +
'<div class="hitl-pending-item-title">' +
'<span class="hitl-tool-badge">' + escapeHtml(item.toolName || '-') + '</span>' +
'<span class="hitl-mode-tag hitl-mode-tag--' + escapeHtml(mode) + '">' + escapeHtml(item.mode || '-') + '</span>' +
'</div>' +
'<div><small>conversation: ' + escapeHtml(item.conversationId || '-') + '</small></div>' +
'<pre style="white-space:pre-wrap;max-height:160px;overflow:auto;">' + escapeHtml(preview) + '</pre>' +
'<button class="hitl-dismiss-btn" title="忽略" onclick="dismissHitlItem(' + qId + ')">&times;</button>' +
'</div>' +
'<div class="hitl-pending-meta">会话:' + escapeHtml(item.conversationId || '-') + '</div>' +
'<pre class="hitl-pending-payload">' + escapeHtml(preview) + '</pre>' +
(allowEdit
? ('<div class="hitl-input-help">审查编辑模式:可填写 JSON 对象覆盖参数示例:{"command":"ls -la"}</div>' +
'<textarea id="hitl-edit-' + escapeHtml(String(item.id || '')) + '" class="hitl-edit-args" placeholder=\'{"command":"ls -la"}\'></textarea>')
? ('<div class="hitl-input-help">审查编辑模式:可填写 JSON 对象覆盖参数示例:{"command":"ls -la"}</div>' +
'<textarea id="hitl-edit-' + escId + '" class="hitl-edit-args" placeholder=\'{"command":"ls -la"}\'></textarea>')
: '<div class="hitl-input-help">审批模式:仅通过/拒绝,不支持改参。</div>') +
'<div class="hitl-input-help">备注(可选):建议写审批依据。</div>' +
'<input id="hitl-comment-' + escId + '" class="hitl-config-input hitl-inline-comment" type="text" placeholder="例如:允许只读命令">' +
'<div class="hitl-pending-actions">' +
'<button class="btn-primary" onclick="submitHitlDecision(' + JSON.stringify(String(item.id || '')) + ',\'approve\',' + JSON.stringify(String(item.conversationId || '')) + ')">通过</button>' +
'<button class="btn-secondary" onclick="submitHitlDecision(' + JSON.stringify(String(item.id || '')) + ',\'reject\',' + JSON.stringify(String(item.conversationId || '')) + ')">拒绝</button>' +
'<button class="btn-secondary" onclick="submitHitlDecision(' + qId + ',&quot;reject&quot;,' + qConv + ')">拒绝</button>' +
'<button class="btn-primary" onclick="submitHitlDecision(' + qId + ',&quot;approve&quot;,' + qConv + ')">通过</button>' +
'</div>' +
'</div>'
);
@@ -307,7 +315,8 @@ async function refreshHitlPending() {
}
async function submitHitlDecision(interruptId, decision, conversationIdOpt) {
const comment = prompt('审批备注(可选)') || '';
const commentBox = document.getElementById('hitl-comment-' + interruptId);
const comment = (commentBox && commentBox.value) ? commentBox.value.trim() : '';
let editedArguments = null;
const editBox = document.getElementById('hitl-edit-' + interruptId);
if (editBox && editBox.value && editBox.value.trim()) {
@@ -332,7 +341,7 @@ async function submitHitlDecisionWithPayload(interruptId, decision, comment, edi
if (!resp.ok) {
const errText = await readHitlApiError(resp);
if (resp.status === 409 && (errText.indexOf('already resolved') >= 0 || errText.indexOf('not found') >= 0)) {
refreshHitlPending();
await dismissHitlItem(interruptId, true);
return true;
}
alert('提交失败:' + errText);
@@ -363,9 +372,24 @@ async function readHitlApiError(resp) {
}
}
async function dismissHitlItem(interruptId, silent) {
try {
await hitlApiFetch('/api/hitl/dismiss', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interruptId: interruptId })
});
} catch (e) {
if (!silent) { console.warn('dismissHitlItem', e); }
}
refreshHitlPending();
}
window.refreshHitlPending = refreshHitlPending;
window.submitHitlDecision = submitHitlDecision;
window.submitHitlDecisionWithPayload = submitHitlDecisionWithPayload;
window.dismissHitlItem = dismissHitlItem;
window.followAgentRunAfterHitlDecision = followAgentRunAfterHitlDecision;
window.addEventListener('hitl-interrupt', function () {
+179 -72
View File
@@ -3,6 +3,36 @@ let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
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 标签(与时间格式化一致)
function getCurrentTimeLocale() {
if (typeof window.__locale === 'string' && window.__locale.length) {
@@ -934,6 +964,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 更新当前对话ID
currentConversationId = event.data.conversationId;
syncAgentLiveStreamConversationId(event.data.conversationId);
updateActiveConversation();
addAttackChainButton(currentConversationId);
loadActiveTasks();
@@ -1120,6 +1151,49 @@ function handleStreamEvent(event, progressElement, progressId,
break;
}
case 'eino_stream_error': {
const d = event.data || {};
const agent = d.einoAgent ? String(d.einoAgent) : '';
const title = typeof window.t === 'function'
? window.t('chat.einoStreamErrorTitle', { agent: agent || '-' })
: (agent ? ('⚠️ Eino 流式中断(' + agent + '') : '⚠️ Eino 流式中断');
addTimelineItem(timeline, 'warning', {
title: title,
message: event.message || (typeof window.t === 'function'
? window.t('chat.einoStreamErrorMessage')
: '流式读取异常,系统将按策略重试或结束。'),
data: d
});
break;
}
case 'iteration_limit_reached': {
addTimelineItem(timeline, 'warning', {
title: typeof window.t === 'function' ? window.t('chat.iterationLimitReachedTitle') : '⛔ 达到迭代上限',
message: event.message || (typeof window.t === 'function'
? window.t('chat.iterationLimitReachedMessage')
: '已达到最大迭代次数,任务已停止继续自动迭代。'),
data: event.data
});
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
break;
}
case 'eino_pending_orphaned': {
const d = event.data || {};
const count = Number(d.pendingCount || 0);
const countText = Number.isFinite(count) && count > 0 ? String(count) : '?';
addTimelineItem(timeline, 'warning', {
title: typeof window.t === 'function' ? window.t('chat.einoPendingOrphanedTitle') : '🧹 工具调用收尾补偿',
message: event.message || (typeof window.t === 'function'
? window.t('chat.einoPendingOrphanedMessage', { count: countText })
: ('检测到 ' + countText + ' 个未闭合工具调用,已自动标记为失败并收尾。')),
data: d
});
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
break;
}
case 'tool_call':
const toolInfo = event.data || {};
const toolName = toolInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
@@ -1429,6 +1503,7 @@ function handleStreamEvent(event, progressElement, progressId,
break;
}
currentConversationId = responseData.conversationId;
syncAgentLiveStreamConversationId(responseData.conversationId);
updateActiveConversation();
addAttackChainButton(currentConversationId);
updateProgressConversation(progressId, responseData.conversationId);
@@ -1509,6 +1584,7 @@ function handleStreamEvent(event, progressElement, progressId,
}
currentConversationId = responseData.conversationId;
syncAgentLiveStreamConversationId(responseData.conversationId);
updateActiveConversation();
addAttackChainButton(currentConversationId);
updateProgressConversation(progressId, responseData.conversationId);
@@ -1639,6 +1715,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 更新对话ID
if (event.data && event.data.conversationId) {
currentConversationId = event.data.conversationId;
syncAgentLiveStreamConversationId(event.data.conversationId);
updateActiveConversation();
addAttackChainButton(currentConversationId);
updateProgressConversation(progressId, event.data.conversationId);
@@ -1939,90 +2016,120 @@ async function refreshLastAssistantProcessDetails(conversationId) {
window.refreshLastAssistantProcessDetails = refreshLastAssistantProcessDetails;
const taskEventReplayAttachState = {
conversationId: null,
inFlightPromise: null
};
/**
* 订阅运行中任务的 SSE 镜像GET /api/agent-loop/task-events用于 HITL 通过后主连接已断开时接续 UI
*/
async function attachRunningTaskEventStream(conversationId) {
if (!conversationId || typeof apiFetch !== 'function') return false;
try {
const check = await apiFetch('/api/agent-loop/tasks');
if (!check.ok) return false;
const j = await check.json().catch(function () { return {}; });
const active = (j.tasks || []).some(function (t) {
return t && t.conversationId === conversationId && (t.status === 'running' || t.status === 'cancelling');
});
if (!active) return false;
if (
taskEventReplayAttachState.inFlightPromise &&
taskEventReplayAttachState.conversationId === conversationId
) {
return taskEventReplayAttachState.inFlightPromise;
}
if (shouldSkipTaskEventReplayAttach(conversationId)) {
return false;
}
const asEl = findLastAssistantMessageElInChat();
if (!asEl || !asEl.id) return false;
const backendId = asEl.dataset && asEl.dataset.backendMessageId;
if (backendId && typeof renderProcessDetails === 'function') {
const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
const jd = await res.json().catch(function () { return {}; });
if (res.ok && Array.isArray(jd.processDetails)) {
renderProcessDetails(asEl.id, jd.processDetails);
}
}
expandProcessDetailsTimeline(asEl.id);
const attachPromise = (async function () {
try {
const check = await apiFetch('/api/agent-loop/tasks');
if (!check.ok) return false;
const j = await check.json().catch(function () { return {}; });
const active = (j.tasks || []).some(function (t) {
return t && t.conversationId === conversationId && (t.status === 'running' || t.status === 'cancelling');
});
if (!active) return false;
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);
const asEl = findLastAssistantMessageElInChat();
if (!asEl || !asEl.id) return false;
const backendId = asEl.dataset && asEl.dataset.backendMessageId;
if (backendId && typeof renderProcessDetails === 'function') {
const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
const jd = await res.json().catch(function () { return {}; });
if (res.ok && Array.isArray(jd.processDetails)) {
renderProcessDetails(asEl.id, jd.processDetails);
// renderProcessDetails 会重建时间线节点,需重新挂载 HITL 审批入口
if (typeof window.restoreHitlInlineForConversation === 'function') {
await window.restoreHitlInlineForConversation(conversationId);
}
}
}
}
if (window.csTaskReplay && window.csTaskReplay.progressId === progressId) {
expandProcessDetailsTimeline(asEl.id);
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();
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') : '已完成');
}
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();
return false;
}
})();
taskEventReplayAttachState.conversationId = conversationId;
taskEventReplayAttachState.inFlightPromise = attachPromise;
return attachPromise;
}
window.attachRunningTaskEventStream = attachRunningTaskEventStream;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

After

Width:  |  Height:  |  Size: 85 KiB

+27 -24
View File
@@ -117,10 +117,9 @@
<div class="nav-item" data-page="hitl">
<div class="nav-item-content" data-title="人机协同" onclick="switchPage('hitl')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="7" r="3"></circle>
<circle cx="16" cy="7" r="3"></circle>
<path d="M2 20c0-3 2.5-5 6-5s6 2 6 5"></path>
<path d="M10 20h12"></path>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<polyline points="16 11 18 13 22 9"></polyline>
</svg>
<span data-i18n="nav.hitl">人机协同</span>
</div>
@@ -511,8 +510,8 @@
<div id="conversations-list" class="conversations-list"></div>
</div>
</div>
<div class="hitl-sidebar-card" id="hitl-sidebar-card">
<div class="hitl-sidebar-card-header">
<div class="hitl-sidebar-card hitl-sidebar-collapsed" id="hitl-sidebar-card">
<div class="hitl-sidebar-card-header" onclick="toggleHitlSidebarCard()">
<div class="hitl-sidebar-heading">
<span class="hitl-sidebar-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -525,24 +524,28 @@
<span class="hitl-sidebar-subtitle" data-i18n="chat.hitlCardSubtitle">审批与白名单</span>
</div>
</div>
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
<span data-i18n="chat.hitlApply">应用</span>
</button>
</div>
<div id="hitl-apply-feedback" class="hitl-apply-feedback" role="status" aria-live="polite"></div>
<div class="hitl-sidebar-config">
<div class="hitl-config-field">
<label class="hitl-config-label" for="hitl-mode-select" data-i18n="chat.hitlModeLabel">模式</label>
<select id="hitl-mode-select" class="hitl-config-select">
<option value="off" data-i18n="chat.hitlModeOff">关闭</option>
<option value="approval" data-i18n="chat.hitlModeApproval">审批模式</option>
<option value="review_edit" data-i18n="chat.hitlModeReviewEdit">审查编辑</option>
</select>
<div class="hitl-sidebar-header-actions">
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="event.stopPropagation(); window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
<span data-i18n="chat.hitlApply">应用</span>
</button>
</div>
<div class="hitl-config-field hitl-config-field--tools">
<label class="hitl-config-label" for="hitl-sensitive-tools" data-i18n="chat.hitlWhitelistTools">白名单工具(免审批,逗号分隔)</label>
<textarea id="hitl-sensitive-tools" class="hitl-config-textarea" rows="3" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
</div>
<div class="hitl-sidebar-body" id="hitl-sidebar-body">
<div id="hitl-apply-feedback" class="hitl-apply-feedback" role="status" aria-live="polite"></div>
<div class="hitl-sidebar-config">
<div class="hitl-config-field">
<label class="hitl-config-label" for="hitl-mode-select" data-i18n="chat.hitlModeLabel">模式</label>
<select id="hitl-mode-select" class="hitl-config-select">
<option value="off" data-i18n="chat.hitlModeOff">关闭</option>
<option value="approval" data-i18n="chat.hitlModeApproval">审批模式</option>
<option value="review_edit" data-i18n="chat.hitlModeReviewEdit">审查编辑</option>
</select>
</div>
<div class="hitl-config-field hitl-config-field--tools">
<label class="hitl-config-label" for="hitl-sensitive-tools" data-i18n="chat.hitlWhitelistTools">白名单工具(免审批,逗号分隔)</label>
<textarea id="hitl-sensitive-tools" class="hitl-config-textarea" rows="3" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
</div>
</div>
</div>
</div>
@@ -731,7 +734,7 @@
</div>
<div class="page-content">
<div class="settings-section">
<h3 data-i18n="hitl.pendingTitle">待处理中断</h3>
<h3 data-i18n="hitl.pendingTitle">待处理审批</h3>
<div id="hitl-pending-list" class="hitl-pending-list"></div>
</div>
</div>