mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb47077082 | |||
| f9a482857d | |||
| 679a68b12f | |||
| 840a26c7ef | |||
| 030e69c02d | |||
| d9683cdb44 | |||
| 60a063dd7d | |||
| 5f0c1805a7 | |||
| cb7e66001b | |||
| 4ea838f1d7 | |||
| 573648fc4b | |||
| f0e090abea | |||
| 549dcf518c | |||
| c74e20c54a | |||
| c94a9fd9e9 | |||
| ce9749a8ef | |||
| 145da12017 | |||
| 5111f4c311 | |||
| 8f6384a083 | |||
| 762f778e1e | |||
| 4a11ba8f14 |
@@ -211,7 +211,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
|
||||
**CyberStrikeAI one-click upgrade (recommended):**
|
||||
1. (First time) enable the script: `chmod +x upgrade.sh`
|
||||
2. Upgrade with: `./upgrade.sh` (optional flags: `--tag vX.Y.Z`, `--no-venv`, `--preserve-custom`, `--yes`)
|
||||
2. Upgrade with: `./upgrade.sh` (optional flags: `--tag vX.Y.Z`, `--no-venv`, `--yes`). Local `tools/`, `roles/`, and `skills/` are always preserved.
|
||||
3. The script will back up your `config.yaml` and `data/`, upgrade the code from GitHub Release, update `config.yaml`'s `version`, then restart the server.
|
||||
|
||||
Recommended one-liner:
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
### CyberStrikeAI 版本更新(无兼容性问题)
|
||||
|
||||
1. (首次使用)启用脚本:`chmod +x upgrade.sh`
|
||||
2. 一键升级:`./upgrade.sh`(可选参数:`--tag vX.Y.Z`、`--no-venv`、`--preserve-custom`、`--yes`)
|
||||
2. 一键升级:`./upgrade.sh`(可选参数:`--tag vX.Y.Z`、`--no-venv`、`--yes`)。本地的 `tools/`、`roles/`、`skills/` 会始终保留不被覆盖。
|
||||
3. 脚本会备份你的 `config.yaml` 和 `data/`,从 GitHub Release 升级代码,更新 `config.yaml` 的 `version` 字段后重启服务。
|
||||
|
||||
推荐的一键指令:
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.6.9"
|
||||
version: "v1.6.12"
|
||||
# 服务器配置
|
||||
server:
|
||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||
@@ -60,10 +60,10 @@ fofa:
|
||||
# Agent 配置
|
||||
# 达到最大迭代次数时,AI 会自动总结测试结果
|
||||
agent:
|
||||
max_iterations: 120 # 最大迭代次数,AI 代理最多执行多少轮工具调用
|
||||
max_iterations: 1200 # 最大迭代次数,AI 代理最多执行多少轮工具调用
|
||||
large_result_threshold: 102400 # 大结果阈值(字节),默认50KB,超过此大小会自动保存到存储
|
||||
result_storage_dir: tmp # 结果存储目录,大结果会保存在此目录下
|
||||
tool_timeout_minutes: 30 # 单次工具执行最大时长(分钟),超时自动终止;0 表示不限制(不推荐,易出现长时间挂起)
|
||||
tool_timeout_minutes: 60 # 单次工具执行最大时长(分钟),超时自动终止;0 表示不限制(不推荐,易出现长时间挂起)
|
||||
# system_prompt_path: prompts/single-react.md # 可选:单代理系统提示文件(相对本配置文件所在目录);非空且可读时替换内置提示
|
||||
# 人机协同(HITL)全局白名单:此处列出的工具始终免审批,与对话页「白名单工具(免审批,逗号分隔)」合并为并集;侧栏「应用」可合并写入本列表并立即生效。
|
||||
hitl:
|
||||
|
||||
@@ -391,7 +391,8 @@ type MultiAgentAPIUpdate struct {
|
||||
RobotUseMultiAgent bool `json:"robot_use_multi_agent"`
|
||||
BatchUseMultiAgent bool `json:"batch_use_multi_agent"`
|
||||
PlanExecuteLoopMaxIterations *int `json:"plan_execute_loop_max_iterations,omitempty"`
|
||||
ToolSearchAlwaysVisibleTools []string `json:"tool_search_always_visible_tools,omitempty"`
|
||||
// 指针区分「JSON 未传该字段」与「传空数组要清空」;省略时不应覆盖 YAML 中的常驻工具白名单。
|
||||
ToolSearchAlwaysVisibleTools *[]string `json:"tool_search_always_visible_tools,omitempty"`
|
||||
}
|
||||
|
||||
// RobotsConfig 机器人配置(企业微信、钉钉、飞书等)
|
||||
|
||||
@@ -755,7 +755,9 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
|
||||
if req.MultiAgent.PlanExecuteLoopMaxIterations != nil {
|
||||
h.config.MultiAgent.PlanExecuteLoopMaxIterations = *req.MultiAgent.PlanExecuteLoopMaxIterations
|
||||
}
|
||||
h.config.MultiAgent.EinoMiddleware.ToolSearchAlwaysVisibleTools = dedupeToolNameList(req.MultiAgent.ToolSearchAlwaysVisibleTools)
|
||||
if req.MultiAgent.ToolSearchAlwaysVisibleTools != nil {
|
||||
h.config.MultiAgent.EinoMiddleware.ToolSearchAlwaysVisibleTools = dedupeToolNameList(*req.MultiAgent.ToolSearchAlwaysVisibleTools)
|
||||
}
|
||||
h.logger.Info("更新多代理配置",
|
||||
zap.Bool("enabled", h.config.MultiAgent.Enabled),
|
||||
zap.Bool("robot_use_multi_agent", h.config.MultiAgent.RobotUseMultiAgent),
|
||||
@@ -1474,6 +1476,11 @@ func updateRobotsConfig(doc *yaml.Node, cfg config.RobotsConfig) {
|
||||
root := doc.Content[0]
|
||||
robotsNode := ensureMap(root, "robots")
|
||||
|
||||
if cfg.Session.StrictUserIdentity != nil {
|
||||
sessionNode := ensureMap(robotsNode, "session")
|
||||
setBoolInMap(sessionNode, "strict_user_identity", *cfg.Session.StrictUserIdentity)
|
||||
}
|
||||
|
||||
wecomNode := ensureMap(robotsNode, "wecom")
|
||||
setBoolInMap(wecomNode, "enabled", cfg.Wecom.Enabled)
|
||||
setStringInMap(wecomNode, "token", cfg.Wecom.Token)
|
||||
@@ -1486,12 +1493,14 @@ func updateRobotsConfig(doc *yaml.Node, cfg config.RobotsConfig) {
|
||||
setBoolInMap(dingtalkNode, "enabled", cfg.Dingtalk.Enabled)
|
||||
setStringInMap(dingtalkNode, "client_id", cfg.Dingtalk.ClientID)
|
||||
setStringInMap(dingtalkNode, "client_secret", cfg.Dingtalk.ClientSecret)
|
||||
setBoolInMap(dingtalkNode, "allow_conversation_id_fallback", cfg.Dingtalk.AllowConversationIDFallback)
|
||||
|
||||
larkNode := ensureMap(robotsNode, "lark")
|
||||
setBoolInMap(larkNode, "enabled", cfg.Lark.Enabled)
|
||||
setStringInMap(larkNode, "app_id", cfg.Lark.AppID)
|
||||
setStringInMap(larkNode, "app_secret", cfg.Lark.AppSecret)
|
||||
setStringInMap(larkNode, "verify_token", cfg.Lark.VerifyToken)
|
||||
setBoolInMap(larkNode, "allow_chat_id_fallback", cfg.Lark.AllowChatIDFallback)
|
||||
}
|
||||
|
||||
func updateMultiAgentConfig(doc *yaml.Node, cfg config.MultiAgentConfig) {
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
|
||||
"cyberstrike-ai/internal/agent"
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/einoobserve"
|
||||
"cyberstrike-ai/internal/einomcp"
|
||||
"cyberstrike-ai/internal/einoobserve"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
|
||||
"github.com/cloudwego/eino/adk"
|
||||
@@ -267,7 +267,16 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
||||
isErr := !success || invokeErr != nil
|
||||
body := content
|
||||
if invokeErr != nil {
|
||||
body = invokeErr.Error()
|
||||
// 保留已流式累计的 stdout(如 execute 超时前的一半输出),避免 tool_result 只剩错误串、模型与 UI 丢失上下文
|
||||
tail := friendlyEinoExecuteInvokeTail(invokeErr)
|
||||
// execute 流式包装可能已把超时句写入 content(供 ADK tool 与流式 delta);勿重复拼接
|
||||
if tail != "" && strings.Contains(content, tail) {
|
||||
body = content
|
||||
} else if strings.TrimSpace(content) != "" {
|
||||
body = strings.TrimRight(content, "\n") + "\n\n" + tail
|
||||
} else {
|
||||
body = tail
|
||||
}
|
||||
isErr = true
|
||||
}
|
||||
recordPendingExecuteStdoutDup(toolName, body, isErr)
|
||||
@@ -948,6 +957,17 @@ func einoPartialRunLastOutputHint() string {
|
||||
"[Run ended abnormally; continue from the trace above without repeating completed steps.]"
|
||||
}
|
||||
|
||||
// friendlyEinoExecuteInvokeTail 将 Eino execute 等非 MCP 路径的结尾错误转成简短提示;其它情况保留原 error 文本。
|
||||
func friendlyEinoExecuteInvokeTail(invokeErr error) string {
|
||||
if invokeErr == nil {
|
||||
return ""
|
||||
}
|
||||
if errors.Is(invokeErr, context.DeadlineExceeded) {
|
||||
return einoExecuteTimeoutUserHint()
|
||||
}
|
||||
return "[执行未正常结束] " + invokeErr.Error()
|
||||
}
|
||||
|
||||
func buildEinoRunResultFromAccumulated(
|
||||
orchMode string,
|
||||
runAccumulatedMsgs []adk.Message,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/einomcp"
|
||||
"cyberstrike-ai/internal/security"
|
||||
@@ -15,6 +16,24 @@ import (
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// prependPythonUnbufferedEnv 为 /bin/sh -c 注入 PYTHONUNBUFFERED=1。
|
||||
// eino-ext local 对流式 stdout 使用 bufio 按「行」推送;python3 写管道时默认块缓冲,print 长期留在用户态缓冲,
|
||||
// 管道里收不到换行,表现为长时间无输出直至超时或退出。若命令里已出现 PYTHONUNBUFFERED 则不再覆盖。
|
||||
func prependPythonUnbufferedEnv(shellCommand string) string {
|
||||
if strings.TrimSpace(shellCommand) == "" {
|
||||
return shellCommand
|
||||
}
|
||||
if strings.Contains(strings.ToUpper(shellCommand), "PYTHONUNBUFFERED") {
|
||||
return shellCommand
|
||||
}
|
||||
return "export PYTHONUNBUFFERED=1\n" + shellCommand
|
||||
}
|
||||
|
||||
// einoExecuteTimeoutUserHint 与写入 ADK 工具消息(模型可见)及 SSE tool_result 尾标一致。
|
||||
func einoExecuteTimeoutUserHint() string {
|
||||
return "已超时终止 · Timed out"
|
||||
}
|
||||
|
||||
// einoStreamingShellWrap 包装 Eino filesystem 使用的 StreamingShell(cloudwego eino-ext local.Local)。
|
||||
// 官方 execute 工具默认走 ExecuteStreaming 且不设 RunInBackendGround;末尾带 & 时子进程仍与管道相连,
|
||||
// streamStdout 按行读取会在无换行输出时长时间阻塞(与 MCP 工具 exec 的独立实现不同)。
|
||||
@@ -29,6 +48,10 @@ type einoStreamingShellWrap struct {
|
||||
inner filesystem.StreamingShell
|
||||
invokeNotify *einomcp.ToolInvokeNotifyHolder
|
||||
einoAgentName string
|
||||
// outputChunk 可选;非 nil 时在收到内层 ExecuteResponse 片段时推送,与 MCP 工具的 tool_result_delta 一致(需有效 toolCallId)。
|
||||
outputChunk func(toolName, toolCallID, chunk string)
|
||||
// toolTimeoutMinutes 与 agent.tool_timeout_minutes 对齐;>0 时对单次 execute 套用 context 超时(与 MCP 工具经 executeToolViaMCP 行为一致)。0 表示仅依赖上层 ctx(如整任务 10h 上限)。
|
||||
toolTimeoutMinutes int
|
||||
// recordMonitor 在 execute 流结束后写入 tool_executions 并 recorder(executionId),使「渗透测试详情」与常规 MCP 一致。
|
||||
recordMonitor func(command, stdout string, success bool, invokeErr error)
|
||||
}
|
||||
@@ -41,17 +64,27 @@ func (w *einoStreamingShellWrap) ExecuteStreaming(ctx context.Context, input *fi
|
||||
return w.inner.ExecuteStreaming(ctx, nil)
|
||||
}
|
||||
req := *input
|
||||
cmd := strings.TrimSpace(req.Command)
|
||||
userCmd := strings.TrimSpace(req.Command)
|
||||
if security.IsBackgroundShellCommand(req.Command) && !req.RunInBackendGround {
|
||||
req.RunInBackendGround = true
|
||||
}
|
||||
req.Command = prependPythonUnbufferedEnv(req.Command)
|
||||
tid := strings.TrimSpace(compose.GetToolCallID(ctx))
|
||||
agentTag := strings.TrimSpace(w.einoAgentName)
|
||||
|
||||
sr, err := w.inner.ExecuteStreaming(ctx, &req)
|
||||
execCtx := ctx
|
||||
var execCancel context.CancelFunc
|
||||
if w.toolTimeoutMinutes > 0 {
|
||||
execCtx, execCancel = context.WithTimeout(ctx, time.Duration(w.toolTimeoutMinutes)*time.Minute)
|
||||
}
|
||||
|
||||
sr, err := w.inner.ExecuteStreaming(execCtx, &req)
|
||||
if err != nil {
|
||||
if execCancel != nil {
|
||||
execCancel()
|
||||
}
|
||||
if w.recordMonitor != nil {
|
||||
w.recordMonitor(cmd, "", false, err)
|
||||
w.recordMonitor(userCmd, "", false, err)
|
||||
}
|
||||
if w.invokeNotify != nil && tid != "" {
|
||||
w.invokeNotify.Fire(tid, "execute", agentTag, false, "", err)
|
||||
@@ -59,13 +92,19 @@ func (w *einoStreamingShellWrap) ExecuteStreaming(ctx context.Context, input *fi
|
||||
return nil, err
|
||||
}
|
||||
if sr == nil || w.invokeNotify == nil || tid == "" {
|
||||
if execCancel != nil {
|
||||
execCancel()
|
||||
}
|
||||
return sr, nil
|
||||
}
|
||||
|
||||
outR, outW := schema.Pipe[*filesystem.ExecuteResponse](32)
|
||||
|
||||
go func(inner *schema.StreamReader[*filesystem.ExecuteResponse], command string) {
|
||||
go func(inner *schema.StreamReader[*filesystem.ExecuteResponse], command string, cancel context.CancelFunc, tctx context.Context) {
|
||||
defer inner.Close()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
const maxCapture = 16 * 1024
|
||||
@@ -90,12 +129,18 @@ func (w *einoStreamingShellWrap) ExecuteStreaming(ctx context.Context, input *fi
|
||||
hasExitCode = true
|
||||
exitCode = *resp.ExitCode
|
||||
}
|
||||
var appended string
|
||||
if remain := maxCapture - sb.Len(); remain > 0 {
|
||||
out := resp.Output
|
||||
if len(out) > remain {
|
||||
out = out[:remain]
|
||||
}
|
||||
sb.WriteString(out)
|
||||
appended = out
|
||||
}
|
||||
// 仅推送写入 sb 的片段,与末尾 Fire/recordMonitor 的截断累计一致,避免最终 tool_result 短于已展示增量。
|
||||
if w.outputChunk != nil && strings.TrimSpace(appended) != "" {
|
||||
w.outputChunk("execute", tid, appended)
|
||||
}
|
||||
if outW.Send(resp, nil) {
|
||||
success = false
|
||||
@@ -109,12 +154,33 @@ func (w *einoStreamingShellWrap) ExecuteStreaming(ctx context.Context, input *fi
|
||||
success = false
|
||||
invokeErr = fmt.Errorf("execute exited with code %d", exitCode)
|
||||
}
|
||||
// WithTimeout 触发后,子进程常被信号结束,local 侧多报 exit -1 / canceled,错误链里不一定带 DeadlineExceeded。
|
||||
// 用执行所用 ctx 归一化,便于 UI 展示「超时」而非含糊的 -1。
|
||||
if tctx != nil && errors.Is(tctx.Err(), context.DeadlineExceeded) {
|
||||
success = false
|
||||
invokeErr = context.DeadlineExceeded
|
||||
}
|
||||
// ADK 从本 Pipe 拼出 tool 消息正文;仅 Notify 尾标不会进入模型上下文。超时句写入流,与 UI 一致。
|
||||
if invokeErr != nil && errors.Is(invokeErr, context.DeadlineExceeded) {
|
||||
hint := "\n\n" + einoExecuteTimeoutUserHint() + "\n"
|
||||
_ = outW.Send(&filesystem.ExecuteResponse{Output: hint}, nil)
|
||||
if w.outputChunk != nil && tid != "" {
|
||||
w.outputChunk("execute", tid, hint)
|
||||
}
|
||||
if remain := maxCapture - sb.Len(); remain > 0 {
|
||||
h := hint
|
||||
if len(h) > remain {
|
||||
h = h[:remain]
|
||||
}
|
||||
sb.WriteString(h)
|
||||
}
|
||||
}
|
||||
if w.recordMonitor != nil {
|
||||
w.recordMonitor(command, sb.String(), success, invokeErr)
|
||||
}
|
||||
w.invokeNotify.Fire(tid, "execute", agentTag, success, sb.String(), invokeErr)
|
||||
outW.Close()
|
||||
}(sr, cmd)
|
||||
}(sr, userCmd, execCancel, execCtx)
|
||||
|
||||
return outR, nil
|
||||
}
|
||||
|
||||
@@ -161,6 +161,8 @@ func buildReductionMiddleware(ctx context.Context, mw config.MultiAgentEinoMiddl
|
||||
}
|
||||
|
||||
// prependEinoMiddlewares returns handlers to prepend (outermost first) and optionally replaces tools when tool_search is used.
|
||||
// toolSearchActive is true when the toolsearch middleware was mounted (dynamic tools split off); callers should pass this to
|
||||
// injectToolNamesOnlyInstruction — tool_search is not part of the pre-middleware tools list, so name-scanning alone cannot detect it.
|
||||
func prependEinoMiddlewares(
|
||||
ctx context.Context,
|
||||
mw *config.MultiAgentEinoMiddlewareConfig,
|
||||
@@ -170,16 +172,16 @@ func prependEinoMiddlewares(
|
||||
skillsRoot string,
|
||||
conversationID string,
|
||||
logger *zap.Logger,
|
||||
) (outTools []tool.BaseTool, extraHandlers []adk.ChatModelAgentMiddleware, err error) {
|
||||
) (outTools []tool.BaseTool, extraHandlers []adk.ChatModelAgentMiddleware, toolSearchActive bool, err error) {
|
||||
if mw == nil {
|
||||
return tools, nil, nil
|
||||
return tools, nil, false, nil
|
||||
}
|
||||
outTools = tools
|
||||
|
||||
if mw.PatchToolCallsEffective() {
|
||||
patchMW, perr := patchtoolcalls.New(ctx, &patchtoolcalls.Config{})
|
||||
if perr != nil {
|
||||
return nil, nil, fmt.Errorf("patchtoolcalls: %w", perr)
|
||||
return nil, nil, false, fmt.Errorf("patchtoolcalls: %w", perr)
|
||||
}
|
||||
extraHandlers = append(extraHandlers, patchMW)
|
||||
}
|
||||
@@ -190,7 +192,7 @@ func prependEinoMiddlewares(
|
||||
} else {
|
||||
redMW, rerr := buildReductionMiddleware(ctx, *mw, conversationID, einoLoc, logger)
|
||||
if rerr != nil {
|
||||
return nil, nil, rerr
|
||||
return nil, nil, false, rerr
|
||||
}
|
||||
extraHandlers = append(extraHandlers, redMW)
|
||||
}
|
||||
@@ -209,10 +211,11 @@ func prependEinoMiddlewares(
|
||||
if split && len(dynamic) > 0 {
|
||||
ts, terr := toolsearch.New(ctx, &toolsearch.Config{DynamicTools: dynamic})
|
||||
if terr != nil {
|
||||
return nil, nil, fmt.Errorf("toolsearch: %w", terr)
|
||||
return nil, nil, false, fmt.Errorf("toolsearch: %w", terr)
|
||||
}
|
||||
extraHandlers = append(extraHandlers, ts)
|
||||
outTools = static
|
||||
toolSearchActive = true
|
||||
if logger != nil {
|
||||
logger.Info("eino middleware: tool_search enabled",
|
||||
zap.Int("static_tools", len(static)),
|
||||
@@ -233,12 +236,12 @@ func prependEinoMiddlewares(
|
||||
}
|
||||
baseDir := filepath.Join(skillsRoot, rel, sanitizeEinoPathSegment(conversationID))
|
||||
if mk := os.MkdirAll(baseDir, 0o755); mk != nil {
|
||||
return nil, nil, fmt.Errorf("plantask mkdir: %w", mk)
|
||||
return nil, nil, toolSearchActive, fmt.Errorf("plantask mkdir: %w", mk)
|
||||
}
|
||||
ptBE := &localPlantaskBackend{Local: einoLoc}
|
||||
pt, perr := plantask.New(ctx, &plantask.Config{Backend: ptBE, BaseDir: baseDir})
|
||||
if perr != nil {
|
||||
return nil, nil, fmt.Errorf("plantask: %w", perr)
|
||||
return nil, nil, toolSearchActive, fmt.Errorf("plantask: %w", perr)
|
||||
}
|
||||
extraHandlers = append(extraHandlers, pt)
|
||||
if logger != nil {
|
||||
@@ -247,7 +250,7 @@ func prependEinoMiddlewares(
|
||||
}
|
||||
}
|
||||
|
||||
return outTools, extraHandlers, nil
|
||||
return outTools, extraHandlers, toolSearchActive, nil
|
||||
}
|
||||
|
||||
func deepExtrasFromConfig(ma *config.MultiAgentConfig) (outputKey string, retry *adk.ModelRetryConfig, taskDesc func(context.Context, []adk.Agent) (string, error)) {
|
||||
|
||||
@@ -96,7 +96,7 @@ func RunEinoSingleChatModelAgent(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mainToolsForCfg, mainOrchestratorPre, err := prependEinoMiddlewares(ctx, &ma.EinoMiddleware, einoMWMain, mainTools, einoLoc, skillsRoot, conversationID, logger)
|
||||
mainToolsForCfg, mainOrchestratorPre, singleToolSearchActive, err := prependEinoMiddlewares(ctx, &ma.EinoMiddleware, einoMWMain, mainTools, einoLoc, skillsRoot, conversationID, logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("eino single eino 中间件: %w", err)
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func RunEinoSingleChatModelAgent(
|
||||
}
|
||||
if einoSkillMW != nil {
|
||||
if einoFSTools && einoLoc != nil {
|
||||
fsMw, fsErr := subAgentFilesystemMiddleware(ctx, einoLoc, toolInvokeNotify, einoSingleAgentName, einoExecMonitor)
|
||||
fsMw, fsErr := subAgentFilesystemMiddleware(ctx, einoLoc, toolInvokeNotify, einoSingleAgentName, einoExecMonitor, agentToolTimeoutMinutes(appCfg), toolOutputChunk)
|
||||
if fsErr != nil {
|
||||
return nil, fmt.Errorf("eino single filesystem 中间件: %w", fsErr)
|
||||
}
|
||||
@@ -178,22 +178,15 @@ func RunEinoSingleChatModelAgent(
|
||||
},
|
||||
EmitInternalEvents: true,
|
||||
}
|
||||
ins := injectToolNamesOnlyInstruction(ctx, ag.EinoSingleAgentSystemInstruction(), mainTools)
|
||||
ins := injectToolNamesOnlyInstruction(ctx, ag.EinoSingleAgentSystemInstruction(), mainTools, singleToolSearchActive)
|
||||
if logger != nil {
|
||||
names := collectToolNames(ctx, mainTools)
|
||||
mountedNames := collectToolNames(ctx, mainToolsForCfg)
|
||||
hasToolSearch := false
|
||||
for _, n := range names {
|
||||
if strings.EqualFold(strings.TrimSpace(n), "tool_search") {
|
||||
hasToolSearch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
logger.Info("eino tool-name injection",
|
||||
zap.String("scope", "eino_single"),
|
||||
zap.Int("tool_names", len(names)),
|
||||
zap.Int("mounted_tool_names", len(mountedNames)),
|
||||
zap.Bool("has_tool_search", hasToolSearch),
|
||||
zap.Bool("tool_search_middleware", singleToolSearchActive),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ func subAgentFilesystemMiddleware(
|
||||
invokeNotify *einomcp.ToolInvokeNotifyHolder,
|
||||
einoAgentName string,
|
||||
recordMonitor func(command, stdout string, success bool, invokeErr error),
|
||||
toolTimeoutMinutes int,
|
||||
outputChunk func(toolName, toolCallID, chunk string),
|
||||
) (adk.ChatModelAgentMiddleware, error) {
|
||||
if loc == nil {
|
||||
return nil, nil
|
||||
@@ -89,10 +91,20 @@ func subAgentFilesystemMiddleware(
|
||||
return filesystem.New(ctx, &filesystem.MiddlewareConfig{
|
||||
Backend: loc,
|
||||
StreamingShell: &einoStreamingShellWrap{
|
||||
inner: loc,
|
||||
invokeNotify: invokeNotify,
|
||||
einoAgentName: strings.TrimSpace(einoAgentName),
|
||||
recordMonitor: recordMonitor,
|
||||
inner: loc,
|
||||
invokeNotify: invokeNotify,
|
||||
einoAgentName: strings.TrimSpace(einoAgentName),
|
||||
outputChunk: outputChunk,
|
||||
recordMonitor: recordMonitor,
|
||||
toolTimeoutMinutes: toolTimeoutMinutes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// agentToolTimeoutMinutes 返回 agent.tool_timeout_minutes(与 executeToolViaMCP 一致);cfg 为 nil 时 0。
|
||||
func agentToolTimeoutMinutes(cfg *config.Config) int {
|
||||
if cfg == nil {
|
||||
return 0
|
||||
}
|
||||
return cfg.Agent.ToolTimeoutMinutes
|
||||
}
|
||||
|
||||
@@ -9,34 +9,43 @@ import (
|
||||
|
||||
// injectToolNamesOnlyInstruction prepends a compact tool-name-only section into
|
||||
// the system instruction so the model can reference current callable names.
|
||||
func injectToolNamesOnlyInstruction(ctx context.Context, instruction string, tools []tool.BaseTool) string {
|
||||
// toolSearchMiddlewareActive must be true when prependEinoMiddlewares mounted toolsearch (dynamic tools); do not infer this
|
||||
// by scanning tool names — tool_search is injected by middleware and is usually absent from the pre-split tools list.
|
||||
func injectToolNamesOnlyInstruction(ctx context.Context, instruction string, tools []tool.BaseTool, toolSearchMiddlewareActive bool) string {
|
||||
names := collectToolNames(ctx, tools)
|
||||
if len(names) == 0 {
|
||||
return strings.TrimSpace(instruction)
|
||||
}
|
||||
hasToolSearch := false
|
||||
for _, n := range names {
|
||||
if strings.EqualFold(strings.TrimSpace(n), "tool_search") {
|
||||
hasToolSearch = true
|
||||
break
|
||||
hasToolSearch := toolSearchMiddlewareActive
|
||||
if !hasToolSearch {
|
||||
for _, n := range names {
|
||||
if strings.EqualFold(strings.TrimSpace(n), "tool_search") {
|
||||
hasToolSearch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("以下是当前会话中可调用的工具名称列表(仅名称,无参数定义):\n")
|
||||
sb.WriteString("以下是当前会话绑定的工具名称索引(仅名称,无参数 JSON Schema)。\n")
|
||||
sb.WriteString("说明:若启用了 tool_search,则列表里可能含「非常驻」工具——它们不一定出现在当前轮次下发给模型的工具定义中;在未看到该工具的完整 schema 前,禁止凭名称臆测参数。\n")
|
||||
for _, name := range names {
|
||||
sb.WriteString("- ")
|
||||
sb.WriteString(name)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
sb.WriteString("\n使用规则:\n")
|
||||
sb.WriteString("1) 上述仅为名称列表,不包含参数定义。\n")
|
||||
sb.WriteString("1) 上表仅为名称索引,不含参数定义。禁止猜测参数名、类型、枚举取值或是否必填。\n")
|
||||
if hasToolSearch {
|
||||
sb.WriteString("2) 在调用具体工具前,应先使用 tool_search 查看工具详情与参数要求,再发起调用。\n")
|
||||
sb.WriteString("【强制 / 最高优先级】本会话已启用 tool_search(动态工具池)。凡名称索引里出现、但你在「当前请求所附 tools 定义」中看不到其完整参数 schema 的工具,一律必须先调用 tool_search;为省 token 或赶进度而跳过 tool_search、直接调用业务工具,属于明确禁止的错误流程。\n")
|
||||
sb.WriteString("2) 默认策略:只要对目标工具的参数定义有任何不确定,就先 tool_search;宁可多一次 tool_search,也不要在未见 schema 时盲调业务工具。\n")
|
||||
sb.WriteString("3) 调用顺序:先 tool_search(唯一必填参数 regex_pattern:按工具名匹配的正则,如子串 nuclei 或 ^exact_tool_name$)→ 在后续轮次确认目标工具已出现在 tools 列表且已阅读其 schema → 再发起对该工具的真实调用。\n")
|
||||
sb.WriteString("4) tool_search 的返回仅为匹配到的工具名列表;schema 在解锁后的下一轮才会下发。禁止在 schema 未出现时编造 JSON 参数。\n")
|
||||
sb.WriteString("5) 不要臆造不存在的工具名。\n\n")
|
||||
} else {
|
||||
sb.WriteString("2) 调用具体工具前,请先确认该工具的参数要求;不确定时先澄清再调用。\n")
|
||||
sb.WriteString("2) 调用具体工具前,请先确认该工具的参数要求(以当前请求中的工具定义为准);不确定时先澄清再调用。\n")
|
||||
sb.WriteString("3) 不要臆造不存在的工具名。\n\n")
|
||||
}
|
||||
sb.WriteString("3) 不要臆造不存在的工具名。\n\n")
|
||||
if s := strings.TrimSpace(instruction); s != "" {
|
||||
sb.WriteString(s)
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ func RunDeepAgent(
|
||||
return nil, fmt.Errorf("子代理 %q 工具: %w", id, err)
|
||||
}
|
||||
|
||||
subToolsForCfg, subPre, err := prependEinoMiddlewares(ctx, &ma.EinoMiddleware, einoMWSub, subTools, einoLoc, skillsRoot, conversationID, logger)
|
||||
subToolsForCfg, subPre, subToolSearchActive, err := prependEinoMiddlewares(ctx, &ma.EinoMiddleware, einoMWSub, subTools, einoLoc, skillsRoot, conversationID, logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("子代理 %q eino 中间件: %w", id, err)
|
||||
}
|
||||
@@ -244,7 +244,7 @@ func RunDeepAgent(
|
||||
}
|
||||
if einoSkillMW != nil {
|
||||
if einoFSTools && einoLoc != nil {
|
||||
subFs, fsErr := subAgentFilesystemMiddleware(ctx, einoLoc, toolInvokeNotify, id, einoExecMonitor)
|
||||
subFs, fsErr := subAgentFilesystemMiddleware(ctx, einoLoc, toolInvokeNotify, id, einoExecMonitor, agentToolTimeoutMinutes(appCfg), toolOutputChunk)
|
||||
if fsErr != nil {
|
||||
return nil, fmt.Errorf("子代理 %q filesystem 中间件: %w", id, fsErr)
|
||||
}
|
||||
@@ -260,23 +260,16 @@ func RunDeepAgent(
|
||||
subHandlers = append(subHandlers, teleMw)
|
||||
}
|
||||
|
||||
subInstrFinal := injectToolNamesOnlyInstruction(ctx, instr, subTools)
|
||||
subInstrFinal := injectToolNamesOnlyInstruction(ctx, instr, subTools, subToolSearchActive)
|
||||
if logger != nil {
|
||||
subNames := collectToolNames(ctx, subTools)
|
||||
mountedNames := collectToolNames(ctx, subToolsForCfg)
|
||||
hasToolSearch := false
|
||||
for _, n := range subNames {
|
||||
if strings.EqualFold(strings.TrimSpace(n), "tool_search") {
|
||||
hasToolSearch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
logger.Info("eino tool-name injection",
|
||||
zap.String("scope", "sub_agent"),
|
||||
zap.String("agent", id),
|
||||
zap.Int("tool_names", len(subNames)),
|
||||
zap.Int("mounted_tool_names", len(mountedNames)),
|
||||
zap.Bool("has_tool_search", hasToolSearch),
|
||||
zap.Bool("tool_search_middleware", subToolSearchActive),
|
||||
)
|
||||
}
|
||||
sa, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||
@@ -341,28 +334,21 @@ func RunDeepAgent(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mainToolsForCfg, mainOrchestratorPre, err := prependEinoMiddlewares(ctx, &ma.EinoMiddleware, einoMWMain, mainTools, einoLoc, skillsRoot, conversationID, logger)
|
||||
mainToolsForCfg, mainOrchestratorPre, mainToolSearchActive, err := prependEinoMiddlewares(ctx, &ma.EinoMiddleware, einoMWMain, mainTools, einoLoc, skillsRoot, conversationID, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orchInstruction = injectToolNamesOnlyInstruction(ctx, orchInstruction, mainTools)
|
||||
orchInstruction = injectToolNamesOnlyInstruction(ctx, orchInstruction, mainTools, mainToolSearchActive)
|
||||
if logger != nil {
|
||||
mainNames := collectToolNames(ctx, mainTools)
|
||||
mountedNames := collectToolNames(ctx, mainToolsForCfg)
|
||||
hasToolSearch := false
|
||||
for _, n := range mainNames {
|
||||
if strings.EqualFold(strings.TrimSpace(n), "tool_search") {
|
||||
hasToolSearch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
logger.Info("eino tool-name injection",
|
||||
zap.String("scope", "orchestrator"),
|
||||
zap.String("orchestration", orchMode),
|
||||
zap.Int("tool_names", len(mainNames)),
|
||||
zap.Int("mounted_tool_names", len(mountedNames)),
|
||||
zap.Bool("has_tool_search", hasToolSearch),
|
||||
zap.Bool("tool_search_middleware", mainToolSearchActive),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -390,10 +376,12 @@ func RunDeepAgent(
|
||||
if einoLoc != nil && einoFSTools {
|
||||
deepBackend = einoLoc
|
||||
deepShell = &einoStreamingShellWrap{
|
||||
inner: einoLoc,
|
||||
invokeNotify: toolInvokeNotify,
|
||||
einoAgentName: orchestratorName,
|
||||
recordMonitor: einoExecMonitor,
|
||||
inner: einoLoc,
|
||||
invokeNotify: toolInvokeNotify,
|
||||
einoAgentName: orchestratorName,
|
||||
outputChunk: toolOutputChunk,
|
||||
recordMonitor: einoExecMonitor,
|
||||
toolTimeoutMinutes: agentToolTimeoutMinutes(appCfg),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,7 +445,7 @@ func RunDeepAgent(
|
||||
// 构建 filesystem 中间件(与 Deep sub-agent 一致)
|
||||
var peFsMw adk.ChatModelAgentMiddleware
|
||||
if einoSkillMW != nil && einoFSTools && einoLoc != nil {
|
||||
peFsMw, err = subAgentFilesystemMiddleware(ctx, einoLoc, toolInvokeNotify, "executor", einoExecMonitor)
|
||||
peFsMw, err = subAgentFilesystemMiddleware(ctx, einoLoc, toolInvokeNotify, "executor", einoExecMonitor, agentToolTimeoutMinutes(appCfg), toolOutputChunk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plan_execute filesystem 中间件: %w", err)
|
||||
}
|
||||
|
||||
+8
-23
@@ -8,11 +8,8 @@ set -euo pipefail
|
||||
# - data/
|
||||
# - venv/ (disabled with --no-venv)
|
||||
# - tools/ (user extensions; never overwritten by upgrade)
|
||||
#
|
||||
# Optional preserves (may overwrite upstream updates):
|
||||
# - roles/
|
||||
# - skills/
|
||||
# Enable with --preserve-custom
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
@@ -28,7 +25,6 @@ BACKUP_BASE_DIR="$ROOT_DIR/.upgrade-backup"
|
||||
GITHUB_REPO="Ed1s0nZ/CyberStrikeAI"
|
||||
|
||||
TAG=""
|
||||
PRESERVE_CUSTOM=0
|
||||
PRESERVE_VENV=1
|
||||
STOP_SERVICE=1
|
||||
FORCE_STOP=0
|
||||
@@ -37,14 +33,12 @@ YES=0
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
./upgrade.sh [--tag vX.Y.Z] [--preserve-custom] [--no-venv] [--no-stop]
|
||||
./upgrade.sh [--tag vX.Y.Z] [--no-venv] [--no-stop]
|
||||
[--force-stop] [--yes]
|
||||
|
||||
Options:
|
||||
--tag <tag> Specify GitHub Release tag (e.g. v1.3.28).
|
||||
If omitted, the script uses the latest release.
|
||||
--preserve-custom Preserve roles/skills (may overwrite upstream files).
|
||||
tools/ is always preserved. Use with caution.
|
||||
--no-venv Do not preserve venv/ (Python deps will be re-installed).
|
||||
--no-stop Do not try to stop the running service.
|
||||
--force-stop If no process matching current directory is found, also stop
|
||||
@@ -52,7 +46,7 @@ Options:
|
||||
--yes Do not ask for confirmation.
|
||||
|
||||
Description:
|
||||
The script backs up config.yaml/data/tools/ (and optionally venv/roles/skills) to
|
||||
The script backs up config.yaml/data/tools/roles/skills/ (and optionally venv/) to
|
||||
.upgrade-backup/
|
||||
EOF
|
||||
}
|
||||
@@ -177,11 +171,7 @@ confirm_or_exit() {
|
||||
info " - Preserve venv/: no (will remove old venv and re-install deps)"
|
||||
fi
|
||||
info " - Preserve tools/: yes (always)"
|
||||
if [[ "$PRESERVE_CUSTOM" -eq 1 ]]; then
|
||||
info " - Preserve roles/skills: yes (may overwrite upstream updates)"
|
||||
else
|
||||
info " - Preserve roles/skills: no (will use upstream versions)"
|
||||
fi
|
||||
info " - Preserve roles/skills: yes (always)"
|
||||
info " - Stop service: ${STOP_SERVICE}"
|
||||
echo ""
|
||||
read -r -p "Continue? (y/N) " ans
|
||||
@@ -299,11 +289,8 @@ sync_code() {
|
||||
|
||||
# User tool extensions: never replace or delete during upgrade.
|
||||
rsync_excludes+=( "--exclude=tools/" )
|
||||
|
||||
if [[ "$PRESERVE_CUSTOM" -eq 1 ]]; then
|
||||
rsync_excludes+=( "--exclude=roles/" )
|
||||
rsync_excludes+=( "--exclude=skills/" )
|
||||
fi
|
||||
rsync_excludes+=( "--exclude=roles/" )
|
||||
rsync_excludes+=( "--exclude=skills/" )
|
||||
|
||||
# Ensure this upgrade script itself is not deleted.
|
||||
rsync_excludes+=( "--exclude=upgrade.sh" )
|
||||
@@ -324,10 +311,6 @@ main() {
|
||||
TAG="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--preserve-custom)
|
||||
PRESERVE_CUSTOM=1
|
||||
shift 1
|
||||
;;
|
||||
--no-venv)
|
||||
PRESERVE_VENV=0
|
||||
shift 1
|
||||
@@ -384,8 +367,10 @@ main() {
|
||||
if [[ -d "$ROOT_DIR/tools" ]]; then
|
||||
backup_dir_tgz "tools" "$ROOT_DIR/tools"
|
||||
fi
|
||||
if [[ "$PRESERVE_CUSTOM" -eq 1 ]]; then
|
||||
if [[ -d "$ROOT_DIR/roles" ]]; then
|
||||
backup_dir_tgz "roles" "$ROOT_DIR/roles"
|
||||
fi
|
||||
if [[ -d "$ROOT_DIR/skills" ]]; then
|
||||
backup_dir_tgz "skills" "$ROOT_DIR/skills"
|
||||
fi
|
||||
|
||||
|
||||
+79
-24
@@ -3708,18 +3708,14 @@ header {
|
||||
|
||||
.timeline-item-iteration {
|
||||
border-left-color: var(--accent-color);
|
||||
background: rgba(0, 102, 255, 0.05);
|
||||
background: rgba(0, 102, 255, 0.06);
|
||||
}
|
||||
|
||||
/* Eino 多代理:主编排器 vs 子代理时间线区分 */
|
||||
.timeline-eino-role-orchestrator {
|
||||
border-left-color: #5c6bc0 !important;
|
||||
background: rgba(92, 107, 192, 0.09) !important;
|
||||
}
|
||||
.timeline-eino-role-sub {
|
||||
border-left-color: #00897b !important;
|
||||
background: rgba(0, 137, 123, 0.08) !important;
|
||||
}
|
||||
/*
|
||||
* Eino 主/子代理:保留 timeline-eino-role-* class(由 applyEinoTimelineRole 写入),
|
||||
* 但不再在此处整卡铺色 + !important,否则会盖住工具调用/结果/思考的类型色。
|
||||
* 主编排 vs 子代理的区分由「迭代轮次」上的 timeline-eino-scope-* 负责。
|
||||
*/
|
||||
.timeline-item-iteration.timeline-eino-scope-main {
|
||||
border-left-color: #3949ab !important;
|
||||
background: rgba(57, 73, 171, 0.1) !important;
|
||||
@@ -3729,29 +3725,72 @@ header {
|
||||
background: rgba(0, 105, 92, 0.09) !important;
|
||||
}
|
||||
|
||||
/* 模型内部思考:弱化灰紫,避免与「助手输出」抢视觉 */
|
||||
.timeline-item-thinking {
|
||||
border-left-color: #9c27b0;
|
||||
background: rgba(156, 39, 176, 0.05);
|
||||
border-left-color: #7e57c2;
|
||||
background: rgba(103, 58, 183, 0.06);
|
||||
}
|
||||
|
||||
/* 迭代中主通道流式正文(标题常为「助手输出」等):中性底 + 主色条,表示对用户可见的答复流 */
|
||||
.timeline-item-thinking[data-response-stream-placeholder="1"] {
|
||||
border-left-color: var(--accent-color);
|
||||
background: rgba(0, 102, 255, 0.04);
|
||||
}
|
||||
|
||||
.timeline-item-reasoning_chain {
|
||||
border-left-color: #5c6bc0;
|
||||
background: rgba(92, 107, 192, 0.06);
|
||||
border-left-color: #5e35b1;
|
||||
background: rgba(94, 53, 177, 0.07);
|
||||
}
|
||||
|
||||
.timeline-item-planning {
|
||||
border-left-color: #00838f;
|
||||
background: rgba(0, 131, 143, 0.06);
|
||||
}
|
||||
|
||||
/* 工具调用:信息色(蓝),与「结果绿/红」分离;完成态不再用绿色以免与成功结果混淆 */
|
||||
.timeline-item-tool_call {
|
||||
border-left-color: #ff9800;
|
||||
background: rgba(255, 152, 0, 0.05);
|
||||
border-left-color: #1565c0;
|
||||
background: rgba(21, 101, 192, 0.07);
|
||||
}
|
||||
|
||||
.timeline-item-tool_result {
|
||||
border-left-color: #78909c;
|
||||
background: rgba(120, 144, 156, 0.06);
|
||||
}
|
||||
|
||||
.timeline-item-tool_result:has(.tool-result-section.success) {
|
||||
border-left-color: var(--success-color);
|
||||
background: rgba(40, 167, 69, 0.05);
|
||||
background: rgba(40, 167, 69, 0.07);
|
||||
}
|
||||
|
||||
.timeline-item-tool_result:has(.tool-result-section.error) {
|
||||
border-left-color: var(--error-color);
|
||||
background: rgba(220, 53, 69, 0.07);
|
||||
}
|
||||
|
||||
.timeline-item-tool_result.error {
|
||||
border-left-color: var(--error-color);
|
||||
background: rgba(220, 53, 69, 0.05);
|
||||
background: rgba(220, 53, 69, 0.07);
|
||||
}
|
||||
|
||||
.timeline-item-eino_agent_reply {
|
||||
border-left-color: #6a1b9a;
|
||||
background: rgba(106, 27, 154, 0.07);
|
||||
}
|
||||
|
||||
.timeline-item-progress {
|
||||
border-left-color: #607d8b;
|
||||
background: rgba(96, 125, 139, 0.08);
|
||||
}
|
||||
|
||||
.timeline-item-warning {
|
||||
border-left-color: #f57c00;
|
||||
background: rgba(245, 124, 0, 0.09);
|
||||
}
|
||||
|
||||
.timeline-item-tool_calls_detected {
|
||||
border-left-color: #0277bd;
|
||||
background: rgba(2, 119, 189, 0.06);
|
||||
}
|
||||
|
||||
.timeline-item-error {
|
||||
@@ -3941,20 +3980,36 @@ header {
|
||||
border: 1px solid rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
/* 工具调用项状态样式 */
|
||||
/* 工具调用项状态:全程保持「信息蓝」系,完成态不用绿色(避免与工具成功结果混淆) */
|
||||
.timeline-item-tool_call.tool-call-running {
|
||||
border-left-color: var(--accent-color);
|
||||
background: rgba(0, 102, 255, 0.08);
|
||||
border-left-color: #42a5f5;
|
||||
background: rgba(66, 165, 245, 0.1);
|
||||
}
|
||||
|
||||
.timeline-item-tool_call.tool-call-completed {
|
||||
border-left-color: var(--success-color);
|
||||
background: rgba(40, 167, 69, 0.08);
|
||||
border-left-color: #0d47a1;
|
||||
background: rgba(13, 71, 161, 0.08);
|
||||
}
|
||||
|
||||
.timeline-item-tool_call.tool-call-failed {
|
||||
border-left-color: var(--error-color);
|
||||
background: rgba(220, 53, 69, 0.08);
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
}
|
||||
|
||||
/* 参数块与卡片类型色弱对齐,扫读时一眼归到「调用」 */
|
||||
.timeline-item-tool_call .tool-args {
|
||||
background: rgba(21, 101, 192, 0.06);
|
||||
border-color: rgba(21, 101, 192, 0.22);
|
||||
}
|
||||
|
||||
.timeline-item-tool_result:has(.tool-result-section.success) .tool-result {
|
||||
background: rgba(40, 167, 69, 0.08);
|
||||
border-color: rgba(40, 167, 69, 0.35);
|
||||
}
|
||||
|
||||
.timeline-item-tool_result:has(.tool-result-section.error) .tool-result {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border-color: rgba(220, 53, 69, 0.45);
|
||||
}
|
||||
|
||||
/* 活跃任务栏 */
|
||||
|
||||
+13
-8
@@ -342,22 +342,27 @@ function formatMarkdown(text) {
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
|
||||
const raw = text == null ? '' : String(text);
|
||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
||||
? window.normalizeAssistantMarkdownSource(raw)
|
||||
: raw;
|
||||
|
||||
if (typeof DOMPurify !== 'undefined') {
|
||||
if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(text)) {
|
||||
if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(src)) {
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
let parsedContent = marked.parse(text);
|
||||
const parsedContent = marked.parse(src, { async: false });
|
||||
return DOMPurify.sanitize(parsedContent, sanitizeConfig);
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return DOMPurify.sanitize(text, sanitizeConfig);
|
||||
return DOMPurify.sanitize(src, sanitizeConfig);
|
||||
}
|
||||
} else {
|
||||
return DOMPurify.sanitize(text, sanitizeConfig);
|
||||
return DOMPurify.sanitize(src, sanitizeConfig);
|
||||
}
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
@@ -365,13 +370,13 @@ function formatMarkdown(text) {
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
return marked.parse(text);
|
||||
return marked.parse(src, { async: false });
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return escapeHtml(text).replace(/\n/g, '<br>');
|
||||
return escapeHtml(src).replace(/\n/g, '<br>');
|
||||
}
|
||||
} else {
|
||||
return escapeHtml(text).replace(/\n/g, '<br>');
|
||||
return escapeHtml(src).replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1844,7 +1844,10 @@ function refreshSystemReadyMessageBubbles() {
|
||||
if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
const parsed = marked.parse(text);
|
||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
||||
? window.normalizeAssistantMarkdownSource(text)
|
||||
: text;
|
||||
const parsed = marked.parse(src, { async: false });
|
||||
formattedContent = typeof DOMPurify !== 'undefined'
|
||||
? DOMPurify.sanitize(parsed, defaultSanitizeConfig)
|
||||
: parsed;
|
||||
@@ -1935,7 +1938,10 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
return marked.parse(raw);
|
||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
||||
? window.normalizeAssistantMarkdownSource(raw)
|
||||
: raw;
|
||||
return marked.parse(src, { async: false });
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return null;
|
||||
|
||||
@@ -726,8 +726,8 @@ function renderDashboardAlertBanner(stats) {
|
||||
try { sessionStorage.setItem(DASH_SESSION_ALERT_LAST_REASONS, reasonPartJoined); } catch (_) {}
|
||||
}
|
||||
|
||||
// External MCP 健康度:从 /api/external-mcp/stats 解析出 running / total / down,
|
||||
// 决定是否在「能力总览」第 6 行显示,并把 down 数返回给 alert banner 驱动告警。
|
||||
// External MCP 健康度:从 /api/external-mcp/stats 解析(后端字段为 total/enabled/disabled/connected),
|
||||
// 决定是否在「能力总览」第 6 行显示,并把「已启用但未连接」的数量返回给 alert banner。
|
||||
function renderExternalMcpHealth(stats) {
|
||||
var row = document.getElementById('dashboard-resource-external-mcp-row');
|
||||
var textEl = document.getElementById('dashboard-resource-external-mcp-text');
|
||||
@@ -738,22 +738,29 @@ function renderExternalMcpHealth(stats) {
|
||||
row.hidden = true;
|
||||
return 0;
|
||||
}
|
||||
// 兼容多种返回字段:{ total, running, stopped/error };常见命名都尝试一下
|
||||
var total = Number(stats.total ?? stats.Total ?? 0) || 0;
|
||||
var running = Number(stats.running ?? stats.Running ?? 0) || 0;
|
||||
var enabled = Number(stats.enabled ?? stats.Enabled ?? 0) || 0;
|
||||
// 后端用 connected 表示已连接数;兼容旧字段 running
|
||||
var connected = Number(stats.connected ?? stats.Connected ??
|
||||
stats.running ?? stats.Running ?? 0) || 0;
|
||||
if (total === 0) {
|
||||
row.hidden = true;
|
||||
return 0;
|
||||
}
|
||||
var down = Math.max(0, total - running);
|
||||
// 未配置任何「已启用」的外部 MCP 时不展示健康行,也不告警(与 MCP 管理页口径一致)
|
||||
if (enabled === 0) {
|
||||
row.hidden = true;
|
||||
return 0;
|
||||
}
|
||||
var down = Math.max(0, enabled - connected);
|
||||
row.hidden = false;
|
||||
textEl.textContent = formatNumber(running) + ' / ' + formatNumber(total);
|
||||
textEl.textContent = formatNumber(connected) + ' / ' + formatNumber(enabled);
|
||||
if (healthEl) {
|
||||
healthEl.classList.remove('is-ok', 'is-warning', 'is-danger');
|
||||
if (down === 0) {
|
||||
healthEl.classList.add('is-ok');
|
||||
healthEl.textContent = dt('dashboard.mcpAllRunning', null, '全部运行');
|
||||
} else if (down < total) {
|
||||
} else if (down < enabled) {
|
||||
healthEl.classList.add('is-warning');
|
||||
healthEl.textContent = dt('dashboard.mcpPartialDown', { count: down },
|
||||
down + ' 个未运行');
|
||||
|
||||
+112
-1
@@ -273,6 +273,116 @@ function escapeHtmlLocal(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/** fenced 块占位(BMP 私用区,正文几乎不会出现) */
|
||||
const _MD_FENCE_PRE = '\n\uE000CSAI_FENCE_';
|
||||
const _MD_FENCE_SUF = '_\uE000\n';
|
||||
|
||||
function _maskFencedCodeBlocksForMdPreprocess(md) {
|
||||
const blocks = [];
|
||||
const masked = String(md).replace(/```[\s\S]*?```/g, (m) => {
|
||||
const i = blocks.length;
|
||||
blocks.push(m);
|
||||
return _MD_FENCE_PRE + i + _MD_FENCE_SUF;
|
||||
});
|
||||
return { masked, blocks };
|
||||
}
|
||||
|
||||
function _unmaskFencedCodeBlocksAfterMdPreprocess(s, blocks) {
|
||||
let out = s;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
out = out.split(_MD_FENCE_PRE + i + _MD_FENCE_SUF).join(blocks[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型/网关偶发把「思考」混进正文,用伪 XML 包裹(如 <redacted_thinking>…</redacted_thinking>)。
|
||||
* 与 Markdown 列表混排时,结束标签常被吞进 <li>,其后 **、` 等行内语法全部无法解析;成对块整段移除。
|
||||
* @param {string} segment
|
||||
* @returns {string}
|
||||
*/
|
||||
function _stripXmlReasoningWrappersForMarkdown(segment) {
|
||||
let t = String(segment);
|
||||
const tags = ['redacted_thinking', 'redacted_reasoning'];
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const name = tags[i];
|
||||
const re = new RegExp('<\\s*' + name + '\\b[^>]*>[\\s\\S]*?<\\s*/\\s*' + name + '\\s*>', 'gi');
|
||||
t = t.replace(re, '\n\n');
|
||||
}
|
||||
return t.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除 LLM 常用的块级 HTML 外壳(`<div>`、`<p>`、`<section>`、`<article>`、`<main>`)。
|
||||
* 整段包在块级标签里时,CommonMark 不会在块内再解析 Markdown,导致 **、` 原样显示。
|
||||
*/
|
||||
function _unwrapHtmlBlockWrappersForMarkdown(segment) {
|
||||
let s = segment;
|
||||
let prev;
|
||||
for (let i = 0; i < 30 && s !== prev; i++) {
|
||||
prev = s;
|
||||
s = s.replace(/<div(?:\s[^>]*)?>([\s\S]*?)<\/div>/gi, (_, inner) => String(inner).trim() + '\n\n');
|
||||
s = s.replace(/<p(?:\s[^>]*)?>([\s\S]*?)<\/p>/gi, (_, inner) => String(inner).trim() + '\n\n');
|
||||
s = s.replace(/<section(?:\s[^>]*)?>([\s\S]*?)<\/section>/gi, (_, inner) => String(inner).trim() + '\n\n');
|
||||
s = s.replace(/<article(?:\s[^>]*)?>([\s\S]*?)<\/article>/gi, (_, inner) => String(inner).trim() + '\n\n');
|
||||
s = s.replace(/<main(?:\s[^>]*)?>([\s\S]*?)<\/main>/gi, (_, inner) => String(inner).trim() + '\n\n');
|
||||
s = s.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 HTML 列表 / 粘连的 `<li>` 还原为 Markdown 列表行,并去掉外层 `<ul>`,便于 marked 解析行内 **、` `
|
||||
* @param {string} segment
|
||||
* @returns {string}
|
||||
*/
|
||||
function _flattenOrphanHtmlLiInMarkdown(segment) {
|
||||
let s = segment;
|
||||
s = s.replace(/<li(?:\s[^>]*)?>([\s\S]*?)<\/li>/gi, (_, inner) => {
|
||||
const body = String(inner).trim().replace(/\s*\n\s*/g, ' ');
|
||||
return '- ' + body + '\n';
|
||||
});
|
||||
s = s.replace(/<\/?ul(?:\s[^>]*)?>/gi, '\n');
|
||||
s = s.replace(/<\/?ol(?:\s[^>]*)?>/gi, '\n');
|
||||
s = s.replace(/([0-9A-Za-z_\u4e00-\u9fff])\s*<li(?:\s[^>]*)?>\s*/g, (_, ch) => ch + '\n- ');
|
||||
return s.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
|
||||
/** 行首 Unicode 项目符号 → Markdown 列表 `- `(模型常用 • 而非 `-`) */
|
||||
function _normalizeUnicodeBulletMarkersToMdDash(segment) {
|
||||
return segment
|
||||
.replace(/^\s*\u2022\s+/gm, '- ')
|
||||
.replace(/^\s*\u00b7\s+/gm, '- ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析前归一化助手 Markdown:去掉零宽字符,NFKC 将全角 * ` _ 等转为 ASCII,
|
||||
* 避免 marked 无法识别强调/行内代码而原样显示 **、反引号;
|
||||
* 并移除 <redacted_thinking> 等伪 XML 思考块、修正块级 HTML(`<div>`/`<p>`/…、`<ul>`/`<li>`)与 Unicode 项目符号 `•`,避免块级 HTML 吞掉 inline 解析。
|
||||
* @param {string|null|undefined} text
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeAssistantMarkdownSource(text) {
|
||||
if (text == null) return '';
|
||||
let s = String(text);
|
||||
s = s.replace(/[\u200B-\u200D\u200E\u200F\uFEFF\u2060]/g, '');
|
||||
try {
|
||||
s = s.normalize('NFKC');
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
s = _stripXmlReasoningWrappersForMarkdown(s);
|
||||
const fb = _maskFencedCodeBlocksForMdPreprocess(s);
|
||||
s = _unwrapHtmlBlockWrappersForMarkdown(fb.masked);
|
||||
s = _flattenOrphanHtmlLiInMarkdown(s);
|
||||
s = _normalizeUnicodeBulletMarkersToMdDash(s);
|
||||
s = _unmaskFencedCodeBlocksAfterMdPreprocess(s, fb.blocks);
|
||||
return s;
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.normalizeAssistantMarkdownSource = normalizeAssistantMarkdownSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 internal/openai.normalizeStreamingDelta 一致:兼容网关/模型返回「累计全文」或整包重发,
|
||||
* 避免前端 buffer += chunk 与后端已归一化的增量叠加导致逐段重复(如「响应中显示了响应中显示了」)。
|
||||
@@ -316,10 +426,11 @@ function setTimelineItemContentStreamRich(contentEl, html) {
|
||||
|
||||
function formatAssistantMarkdownContent(text) {
|
||||
const raw = text == null ? '' : String(text);
|
||||
const src = normalizeAssistantMarkdownSource(raw);
|
||||
if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
const parsed = marked.parse(raw);
|
||||
const parsed = marked.parse(src, { async: false });
|
||||
if (typeof DOMPurify !== 'undefined') {
|
||||
return DOMPurify.sanitize(parsed, assistantMarkdownSanitizeConfig);
|
||||
}
|
||||
|
||||
@@ -1087,6 +1087,7 @@ async function applySettings() {
|
||||
|
||||
const wecomAgentIdVal = document.getElementById('robot-wecom-agent-id')?.value.trim();
|
||||
const prevOpenai = (currentConfig && currentConfig.openai) ? currentConfig.openai : {};
|
||||
const prevRobots = (currentConfig && currentConfig.robots) ? currentConfig.robots : {};
|
||||
const config = {
|
||||
openai: {
|
||||
...prevOpenai,
|
||||
@@ -1118,7 +1119,7 @@ async function applySettings() {
|
||||
return {
|
||||
enabled: document.getElementById('multi-agent-enabled')?.checked === true,
|
||||
robot_use_multi_agent: document.getElementById('multi-agent-robot-use')?.checked === true,
|
||||
batch_use_multi_agent: false,
|
||||
batch_use_multi_agent: currentConfig?.multi_agent?.batch_use_multi_agent === true,
|
||||
plan_execute_loop_max_iterations: peLoop
|
||||
};
|
||||
})(),
|
||||
@@ -1127,6 +1128,7 @@ async function applySettings() {
|
||||
enabled: c2Enabled
|
||||
},
|
||||
robots: {
|
||||
...(prevRobots.session && typeof prevRobots.session === 'object' ? { session: prevRobots.session } : {}),
|
||||
wecom: {
|
||||
enabled: document.getElementById('robot-wecom-enabled')?.checked === true,
|
||||
token: document.getElementById('robot-wecom-token')?.value.trim() || '',
|
||||
@@ -1138,13 +1140,15 @@ async function applySettings() {
|
||||
dingtalk: {
|
||||
enabled: document.getElementById('robot-dingtalk-enabled')?.checked === true,
|
||||
client_id: document.getElementById('robot-dingtalk-client-id')?.value.trim() || '',
|
||||
client_secret: document.getElementById('robot-dingtalk-client-secret')?.value.trim() || ''
|
||||
client_secret: document.getElementById('robot-dingtalk-client-secret')?.value.trim() || '',
|
||||
allow_conversation_id_fallback: !!(prevRobots.dingtalk && prevRobots.dingtalk.allow_conversation_id_fallback)
|
||||
},
|
||||
lark: {
|
||||
enabled: document.getElementById('robot-lark-enabled')?.checked === true,
|
||||
app_id: document.getElementById('robot-lark-app-id')?.value.trim() || '',
|
||||
app_secret: document.getElementById('robot-lark-app-secret')?.value.trim() || '',
|
||||
verify_token: document.getElementById('robot-lark-verify-token')?.value.trim() || ''
|
||||
verify_token: document.getElementById('robot-lark-verify-token')?.value.trim() || '',
|
||||
allow_chat_id_fallback: !!(prevRobots.lark && prevRobots.lark.allow_chat_id_fallback)
|
||||
}
|
||||
},
|
||||
tools: []
|
||||
|
||||
Reference in New Issue
Block a user