diff --git a/docs/MULTI_AGENT_EINO.md b/docs/MULTI_AGENT_EINO.md
deleted file mode 100644
index 76bf3919..00000000
--- a/docs/MULTI_AGENT_EINO.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Eino 多代理改造说明(DeepAgent)
-
-本文档记录 **Eino 单代理(ADK)** 与 **多 Agent(CloudWeGo Eino `adk/prebuilt`)** 的改造范围、进度与后续事项。原生 ReAct 执行路径已移除。
-
-## 总体结论
-
-- **改造已可用于生产试验**:流式对话、MCP 工具桥接、配置开关、前端模式切换均已落地。
-- **入口策略**:**单代理** 走 `/api/eino-agent/stream`;多代理走 `/api/multi-agent/stream`,请求体 **`orchestration`** 指定编排。模式定位按 Eino ADK 最佳实践区分:**Deep** 适合复杂安全测试与 task 子代理协作;**Plan-Execute** 适合目标明确的规划 → 执行 → 重规划闭环;**Supervisor** 适合多个专业子代理动态分派的专家路由场景。机器人默认 `robot_default_agent_mode: eino_single`;批量队列默认 `eino_single`,多代理模式需 `multi_agent.enabled`。
-
-## 已完成项
-
-| 项 | 说明 |
-|----|------|
-| 依赖与代理 | `go.mod` 直接依赖 `github.com/cloudwego/eino`、`eino-ext/.../openai`;`go.mod` 注释与 `scripts/bootstrap-go.sh` 指导 **GOPROXY**(如 `https://goproxy.cn,direct`)。 |
-| 配置 | `config.yaml` → `agent.max_iterations` 为全局 ReAct 上限(主/子代理统一);`multi_agent`:`enabled`、`robot_use_multi_agent`、`sub_agents`(含可选 `bind_role`)、`eino_skills`、`eino_middleware` 等;结构体见 `internal/config/config.go`。 |
-| Markdown 子代理 / 主代理 | 在 `agents_dir` 下放 `*.md`。**子代理**:供 Deep `task` 与 `supervisor` `transfer`。**主代理(按模式分离)**:`orchestrator.md`(或 `kind: orchestrator` 的**单个**其他 .md)→ **Deep**;固定名 `orchestrator-plan-execute.md` → **plan_execute**;固定名 `orchestrator-supervisor.md` → **supervisor**。正文优先于 YAML:`multi_agent.orchestrator_instruction`、`orchestrator_instruction_plan_execute`、`orchestrator_instruction_supervisor`;plan_execute / supervisor **不会**回退到 Deep 的 `orchestrator_instruction`。皆空时 plan_execute / supervisor 使用代码内置默认提示。管理:**Agents → Agent管理**;API:`/api/multi-agent/markdown-agents*`。 |
-| MCP 桥 | `internal/einomcp`:`ToolsFromDefinitions` + 会话 ID 持有者,执行走 `Agent.ExecuteMCPToolForConversation`。 |
-| 编排 | `internal/multiagent/runner.go`:`deep.New` + 子 `ChatModelAgent` + `adk.NewRunner`(`EnableStreaming: true`,可选 `CheckPointStore`),事件映射为现有 SSE `tool_call` / `response_delta` 等。 |
-| HTTP | `POST /api/multi-agent`(非流式)、`POST /api/multi-agent/stream`(SSE);路由**常注册**,是否可用由运行时 `multi_agent.enabled` 决定(流式未启用时 SSE 内 `error` + `done`)。 |
-| 会话准备 | `internal/handler/multi_agent_prepare.go`:`prepareMultiAgentSession`(含 **WebShell** `CreateConversationWithWebshell`、工具白名单与单代理一致)。 |
-| 单 Agent | `internal/agent` 为 MCP/工具层(`ToolsForRole`、`ExecuteMCPToolForConversation`);单代理编排走 `RunEinoSingleChatModelAgent`(`/api/eino-agent*`)。 |
-| 前端 | 主聊天 / WebShell:**Eino 单代理**(`/api/eino-agent/stream`)与 **Deep / Plan-Execute / Supervisor**(`/api/multi-agent/stream` + `orchestration`);`multi_agent.enabled` 控制多代理选项是否展示。 |
-| 流式兼容 | Eino 单/多代理与 Web UI 共用 `handleStreamEvent`:`conversation`、`progress`、`response_start` / `response_delta`、`thinking` / `thinking_stream_*`、`tool_*`、`response`、`done` 等。 |
-| 批量任务 | 队列 `agentMode` 为 `deep` / `plan_execute` / `supervisor` 时子任务带对应 `orchestration` 调用 `RunDeepAgent`;旧值 `multi` 与「`agentMode` 为空且 `batch_use_multi_agent: true`」均按 `deep`。 |
-| 配置 API | `GET /api/config` 返回 `multi_agent: { enabled, robot_use_multi_agent, sub_agent_count }`;`PUT /api/config` 可更新 `enabled`、`robot_use_multi_agent`(不覆盖 `sub_agents`)。 |
-| OpenAPI | 多代理路径说明已更新(流式未启用为 SSE 错误事件)。 |
-| 机器人 | `ProcessMessageForRobot` 按 `robot_default_agent_mode`(默认 `eino_single`)调用 `RunEinoSingleChatModelAgent` 或 `RunDeepAgent`。 |
-| 预置编排 | 聊天 / WebShell:`POST /api/multi-agent*` 请求体 `orchestration`:`deep` \| `plan_execute` \| `supervisor`(缺省 `deep`)。`deep` 使用 task 子代理协作;`plan_execute` 不构建 YAML/Markdown 子代理;`plan_execute_loop_max_iterations` 仍来自配置;`supervisor` 至少需一个子代理,只有一个子代理时会提示其专家路由空间有限。 |
-| Eino 中间件 | `multi_agent.eino_middleware`(可选):`patchtoolcalls`(默认开)、`toolsearch`(按阈值拆分 MCP 工具列表)、`plantask`(需 `eino_skills`)、`reduction`(大工具输出截断/落盘)、`checkpoint_dir`(Runner 断点)、`deep_output_key` / `deep_model_retry_max_retries` / `task_tool_description_prefix`(Deep 与 supervisor 主代理共享其中模型重试与 OutputKey)。**`plan_execute`**:Executor 使用 Eino 官方允许的自定义 `adk.ChatModelAgent`,保持官方 Plan/UserInput/ExecutedSteps session contract,同时挂载与 Deep/Supervisor 主代理同源的 middleware(patch → reduction → toolsearch → plantask → filesystem → skill → summarization tail)。Planner/Replanner 仅 summarization tail + prompt 预算截断,不跑 MCP 工具链。当前 Eino 官方 `planexecute.NewExecutor` 尚未暴露 Handlers 字段,因此该自定义 Executor 是保留 middleware 的对齐实现。 |
-
-## 进行中 / 待办( backlog )
-
-| 优先级 | 项 | 说明 |
-|--------|----|------|
-| P3 | **观测与计费** | Eino 事件可进一步打结构化日志 / trace id,便于排障。 |
-| P3 | **测试** | 增加 `internal/multiagent` 与 einomcp 的集成测试(mock model 或录屏回放)。 |
-
-## 关键文件索引
-
-- `internal/multiagent/runner.go` — DeepAgent / plan_execute / supervisor 组装与事件循环
-- `internal/multiagent/eino_orchestration.go` — PlanExecute 根节点与 Executor 中间件栈(`buildPlanExecuteExecutorHandlers`)
-- `internal/handler/multi_agent.go` — SSE 与(同步)HTTP
-- `internal/handler/multi_agent_prepare.go` — 会话准备(含 WebShell)
-- `internal/einomcp/` — MCP → Eino Tool
-- `config.yaml` — `multi_agent` 示例块
-- `web/static/js/chat.js` — 模式选择与 stream URL
-- `web/static/js/webshell.js` — WebShell AI 流式 URL 与主聊天模式对齐
-- `web/static/js/settings.js` — 多代理标量保存
-
-## 版本记录
-
-| 日期 | 说明 |
-|------|------|
-| 2026-03-22 | 首版:Eino DeepAgent + stream + 前端开关 + GOPROXY 脚本。 |
-| 2026-03-22 | 补充:进度文档、`prepareMultiAgentSession` 抽取、WebShell 后端对齐、`POST /api/multi-agent`、OpenAPI `/api/multi-agent*` 条目。 |
-| 2026-03-22 | 路由常注册、流式未启用 SSE 错误、`robot_use_multi_agent`、设置页持久化、WebShell/机器人多代理、`bind_role` 子代理 Skills/tools。 |
-| 2026-03-22 | `tool_result.toolCallId`、`ReasoningContent`→思考流、`batch_use_multi_agent` 与批量队列 Eino 执行。 |
-| 2026-03-22 | 流式工具事件:按稳定签名去重,避免每 chunk 刷屏与「未知工具」;最终回复去重相同段落;内置调度显示为 `task`。 |
-| 2026-03-22 | `agents/*.md` 子代理定义、`agents_dir`、合并进 `RunDeepAgent`、前端 Agents 菜单与 CRUD API。 |
-| 2026-03-22 | `orchestrator.md` / `kind: orchestrator` 主代理、列表主/子标记、与 `orchestrator_instruction` 优先级。 |
-| 2026-04-19 | 主聊天「对话模式」:原生 ReAct 与 Deep / Plan-Execute / Supervisor;`POST /api/multi-agent*` 请求体 `orchestration` 与界面一致;`config.yaml` / 设置页不再维护预置编排字段(机器人/批量默认 `deep`)。 |
-| 2026-04-21 | 移除角色 `skills` 与 `/api/roles/skills/list`;`bind_role` 仅继承 tools;Skills 仅通过 Eino `skill` 工具按需加载。 |
-| 2026-07-06 | **最佳实践对齐**:Deep / Plan-Execute / Supervisor 改为中性适用场景描述;Supervisor 标为专家路由特定场景并收紧 transfer/exit 约束;plan_execute Executor 明确为遵循官方 session contract 的自定义 ChatModelAgent,保留 middleware 并补类型保护。 |
-| 2026-07-02 | **plan_execute Executor 中间件对齐**:`ExecPreMiddlewares` 与 Deep 主代理同源;`buildPlanExecuteExecutorHandlers` + 回归测试;文档更正。 |
-| 2026-06-02 | **移除原生 ReAct**:删除 `/api/agent-loop*` 执行入口与 `AgentLoopWithProgress`;统一 Eino ADK(单代理 `/api/eino-agent*`,多代理 `/api/multi-agent*`);任务 cancel/tasks API 保留。 |
diff --git a/docs/VISION.md b/docs/VISION.md
deleted file mode 100644
index e7107b40..00000000
--- a/docs/VISION.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# 视觉分析(analyze_image)
-
-## 概述
-
-- **工具名**:`analyze_image`(MCP 内置)
-- **行为**:读取本地图片 → `imaging` 缩放/JPEG 压缩 → 调用独立 **Vision** 模型 → 返回**纯文本**给 Agent
-- **上下文**:图片字节**不会**写入对话历史;仅路径与文字摘要进入 Agent 上下文
-
-## 配置(`config.yaml` → `vision`)
-
-```yaml
-vision:
- enabled: true
- model: qwen-vl-max # 必填
- api_key: # 留空 → openai.api_key
- base_url: # 留空 → openai.base_url
- provider: # 留空 → openai.provider
- max_image_bytes: 5242880
- max_dimension: 2048
- jpeg_quality: 82
- max_payload_bytes: 524288
- skip_preprocess_below_bytes: 2097152 # 低于 2MB 且长边<=max_dimension 时原图直传;0=始终 JPEG 压缩
- detail: low # low | high | auto
- timeout_seconds: 60
-```
-
-`enabled: false` 时不注册工具。
-
-## Web 设置
-
-**系统设置 → 基本设置 → 视觉分析(analyze_image)** 可配置启用开关、视觉模型、API Key/Base URL(留空复用 OpenAI)、预处理参数;**保存并应用** 后写入 `config.yaml` 并重新注册 MCP 工具。
-
-## 路径
-
-`analyze_image` 可读取服务器上任意可读的图片文件路径(绝对路径或相对于进程工作目录的相对路径)。仍校验图片扩展名与常规文件类型。
-
-## Agent 使用
-
-系统提示已说明:遇图片调用 `analyze_image`,勿用 `read_file` 读二进制图。
-
-`multi_agent.eino_middleware.tool_search_always_visible_tools` 建议包含 `analyze_image`。
-
-## 合规
-
-启用后图片会发往 Vision API 配置的上游;敏感环境请使用可信网关或保持 `enabled: false`。
diff --git a/docs/frontend-i18n.md b/docs/frontend-i18n.md
deleted file mode 100644
index 7a9f832f..00000000
--- a/docs/frontend-i18n.md
+++ /dev/null
@@ -1,335 +0,0 @@
-## CyberStrikeAI 前端国际化方案
-
-本文档说明 CyberStrikeAI Web 前端(`web/templates/index.html` + `web/static/js/*.js`)的国际化设计与开发规范,确保在不引入打包工具和不改动后端路由的前提下,实现可扩展、低返工的多语言支持。
-
-当前目标:
-
-- **支持中英文切换(zh-CN / en-US)**
-- 后续可方便扩展更多语言(如 ja-JP、ko-KR 等)
-
----
-
-## 一、总体设计原则
-
-- **前端主导的客户端国际化**:所有 UI 文案在浏览器端根据当前语言动态渲染,后端 Go 仅负责结构和数据,不参与语言分发。
-- **单一 HTML 模板**:继续使用一份 `index.html` 模板,不为不同语言复制模板文件。
-- **文案与逻辑分离**:所有可见文本通过「键值表」管理(多语言 JSON),HTML / JS 只写 key,不直接写中文/英文常量。
-- **渐进式改造**:先覆盖 header / 登录 / 侧边栏 / 系统设置等关键区域,其他页面按模块逐步迁移,避免一次性大改动。
-- **可回退默认语言**:即使目标语言未完全翻译,也能回退到默认中文,不出现原始 key。
-
----
-
-## 二、技术选型与目录结构
-
-### 2.1 技术选型
-
-- **i18n 引擎**:使用 [i18next](https://www.i18next.com/) 的浏览器 UMD 版本(通过 CDN 引入),无需打包器。
-- **资源格式**:每种语言一份 JSON 文件,采用「域 + 语义」的层级 key 方案,例如:
- - `common.ok`
- - `nav.dashboard`
- - `header.apiDocs`
- - `settings.robot.wecom.token`
-
-### 2.2 目录结构
-
-- `web/templates/index.html`
- - 页面骨架 + 所有静态文案位置,将逐步改为 `data-i18n` 标记。
-- `web/static/js/i18n.js`
- - 前端 i18n 初始化与 DOM 应用逻辑(本方案新增)。
-- `web/static/i18n/`(新增目录)
- - `zh-CN.json`:中文文案(默认语言)
- - `en-US.json`:英文文案
- - 未来可新增:`ja-JP.json`、`ko-KR.json` 等。
-
----
-
-## 三、文案组织规范
-
-### 3.1 Key 命名约定
-
-- 采用「**模块.语义**」形式,最多 2–3 级,确保可读性:
- - 导航:`nav.dashboard`、`nav.chat`、`nav.settings`
- - 头部:`header.title`、`header.apiDocs`、`header.logout`
- - 登录:`login.title`、`login.subtitle`、`login.passwordLabel`、`login.submit`
- - 仪表盘:`dashboard.title`、`dashboard.refresh`、`dashboard.runningTasks`
- - 系统设置:`settings.title`、`settings.nav.basic`、`settings.nav.robot`、`settings.apply`
- - 机器人配置:`settings.robot.wecom.enabled`、`settings.robot.wecom.token` 等。
-- 尽量按「界面区域」而不是「文件名」划分域,便于非开发人员理解。
-
-### 3.2 JSON 示例
-
-`web/static/i18n/zh-CN.json` 示例:
-
-```json
-{
- "common": {
- "ok": "确定",
- "cancel": "取消"
- },
- "nav": {
- "dashboard": "仪表盘",
- "chat": "对话",
- "infoCollect": "信息收集",
- "tasks": "任务管理",
- "vulnerabilities": "漏洞管理",
- "settings": "系统设置"
- },
- "header": {
- "title": "CyberStrikeAI",
- "apiDocs": "API 文档",
- "logout": "退出登录",
- "language": "界面语言"
- },
- "login": {
- "title": "登录 CyberStrikeAI",
- "subtitle": "请输入配置中的访问密码",
- "passwordLabel": "密码",
- "passwordPlaceholder": "输入登录密码",
- "submit": "登录"
- }
-}
-```
-
-英文文件 `en-US.json` 保持相同 key,不同 value:
-
-```json
-{
- "common": {
- "ok": "OK",
- "cancel": "Cancel"
- },
- "nav": {
- "dashboard": "Dashboard",
- "chat": "Chat",
- "infoCollect": "Recon",
- "tasks": "Tasks",
- "vulnerabilities": "Vulnerabilities",
- "settings": "Settings"
- },
- "header": {
- "title": "CyberStrikeAI",
- "apiDocs": "API Docs",
- "logout": "Sign out",
- "language": "Interface language"
- },
- "login": {
- "title": "Sign in to CyberStrikeAI",
- "subtitle": "Enter the access password from config",
- "passwordLabel": "Password",
- "passwordPlaceholder": "Enter password",
- "submit": "Sign in"
- }
-}
-```
-
-> 约定:**新增界面时,必须先定义 i18n key,再在 HTML/JS 中使用 key**,禁止直接写死中文/英文。
-
----
-
-## 四、HTML 标记规范(data-i18n)
-
-### 4.1 基本规则
-
-- 使用 `data-i18n` 将元素文本与某个 key 绑定:
-
-```html
-仪表盘
-```
-
-- 默认行为:脚本会替换元素的 `textContent`。
-- 同时翻译属性时,额外使用 `data-i18n-attr`,逗号分隔多个属性名:
-
-```html
-
-```
-
-### 4.2 默认文本的作用
-
-- HTML 内的中文默认值作为「**无 JS / 初始化前**」的占位内容:
- - 页面在 JS 尚未加载完成时不会出现空白或 key。
- - JS 初始化后会用当前语言覆盖这些文本。
-
----
-
-## 五、JavaScript 中的文案规范
-
-### 5.1 全局翻译函数 `t()`
-
-由 `i18n.js` 暴露以下全局函数:
-
-- `window.t(key: string): string`
- - 返回当前语言下的翻译文本,若缺失则回退到默认语言,再不行则返回 key 本身。
-- `window.changeLanguage(lang: string): Promise`
- - 切换语言并刷新页面文案(不会刷新整页)。
-
-示例(以 `web/static/js/settings.js` 为例):
-
-```js
-// 之前
-alert('加载配置失败: ' + error.message);
-
-// 之后
-alert(t('settings.loadConfigFailed') + ': ' + error.message);
-```
-
-> 规范:**JS 内所有面向用户的提示、按钮文字、对话框标题都应通过 `t()` 获取**,不直接写死中文/英文。
-
-### 5.2 渐进迁移建议
-
-- 优先改造:
- - 频繁弹出的错误提示 / 成功提示;
- - 登录相关、系统设置相关文案。
-- 低优先级:
- - 仅面向运维人员的调试提示,可以暂时保留英文/中文常量。
-
----
-
-## 六、i18n 初始化与语言切换实现
-
-### 6.1 语言选择策略
-
-- 默认语言:`zh-CN`。
-- 优先级(从高到低):
- 1. `localStorage` 中的用户选择(key:`csai_lang`)。
- 2. 浏览器 `navigator.language`(`zh` 开头 → `zh-CN`,否则 `en-US`)。
- 3. 默认 `zh-CN`。
-
-### 6.2 初始化流程(`i18n.js`)
-
-1. 读取初始语言。
-2. 初始化 i18next:
- - `lng` 为当前语言;
- - `fallbackLng` 为 `zh-CN`;
- - 资源先留空,采用按需加载。
-3. 通过 `fetch` 拉取 `/static/i18n/{lng}.json` 并 `i18next.addResources`。
-4. 更新:
- - `` 属性;
- - 所有带 `data-i18n` / `data-i18n-attr` 的元素。
-5. 暴露 `window.t` 与 `window.changeLanguage`。
-
-### 6.3 DOM 应用逻辑
-
-伪代码:
-
-```js
-function applyTranslations(root = document) {
- const elements = root.querySelectorAll('[data-i18n]');
- elements.forEach(el => {
- const key = el.getAttribute('data-i18n');
- if (!key) return;
- const text = i18next.t(key);
- if (text) {
- el.textContent = text;
- }
-
- const attrList = el.getAttribute('data-i18n-attr');
- if (attrList) {
- attrList.split(',').map(s => s.trim()).forEach(attr => {
- if (!attr) return;
- const val = i18next.t(key);
- if (val) el.setAttribute(attr, val);
- });
- }
- });
-}
-```
-
-> 对于由 JS 动态插入的元素,需要在插入后再次调用 `applyTranslations(新容器)`。
-
----
-
-## 七、语言切换 UI 规范
-
-### 7.1 位置与形态
-
-- 位置:`index.html` header 右侧 `API 文档` 按钮附近(靠近用户头像)。
-- 交互形式:
- - 一个紧凑的语言切换组件,例如:
- - `🌐` 图标 + 当前语言文本(`中文` / `English`)的下拉按钮;
- - 下拉内容列出所有可用语言。
-
-### 7.2 示例结构
-
-```html
-
-```
-
-对应 JS(在 `i18n.js` 中):
-
-```js
-function onLanguageSelect(lang) {
- changeLanguage(lang).then(updateLangLabel).catch(console.error);
- closeLangDropdown();
-}
-
-function updateLangLabel() {
- const labelEl = document.getElementById('current-lang-label');
- if (!labelEl) return;
- const lang = i18next.language || 'zh-CN';
- labelEl.textContent = lang.startsWith('zh') ? '中文' : 'English';
-}
-```
-
-> 规范:**语言切换只更新文案,不刷新整页,也不修改 URL hash**。
-
----
-
-## 八、开发流程建议
-
-### 8.1 新增 / 修改界面的流程
-
-1. 设计界面时,先列出所有文案。
-2. 在对应语言 JSON 中补充/修改 key 与翻译。
-3. 在 HTML 中使用 `data-i18n`,在 JS 中使用 `t('...')`。
-4. 在浏览器中切换中英文,确认两种语言显示都正确。
-
-### 8.2 渐进式改造顺序(推荐)
-
-1. **阶段 1(已规划)**
- - 引入 i18next 与 `i18n.js`。
- - 新建 `zh-CN.json` / `en-US.json`(先覆盖 header / 登录 / 左侧导航)。
- - 实现 header 区域语言切换组件。
-2. **阶段 2**(已完成)
- - 系统设置页面(包括机器人配置页面)全部文案 i18n 化。
- - `settings.js` 中的提示与错误信息改用 `t()`。
-3. **阶段 3**(进行中)
- - 仪表盘、任务管理、漏洞管理、MCP、Skills、Roles 等页面按模块逐步迁移。
-4. **阶段 4**
- - 清理 JS / HTML 中残留的硬编码中文,统一通过 i18n。
-
----
-
-## 九、后续扩展新语言
-
-当需要新增语言时:
-
-1. 在 `web/static/i18n/` 中新增 `{lang}.json`,复制现有英文/中文文件结构,补充对应翻译。
-2. 在语言切换下拉中添加对应选项,例如:
- - `data-lang="ja-JP"` / 文本 `日本語`
-3. 无需修改 `i18n.js` 或现有 HTML/JS 逻辑,即可支持新语言。
-
----
-
-## 十、注意事项与坑点
-
-- **不要复制多份 HTML 模板** 来做多语言,那样维护成本极高,本方案统一由前端 i18n 控制。
-- **避免 key 直接用中文/英文句子**,统一采用「模块.语义」短 key,便于 diff 与搜索。
-- 避免在 CSS 中写死文本(如 `content: "xxx"`),如确有需要,应通过 JS 设置并走 i18n。
-- 对于后端返回的可本地化错误文本(未来可能支持),优先由后端根据 `Accept-Language` 返回对应语言,前端只负责展示。
-
diff --git a/docs/hitl-best-practices.md b/docs/hitl-best-practices.md
deleted file mode 100644
index 930cd5f1..00000000
--- a/docs/hitl-best-practices.md
+++ /dev/null
@@ -1,122 +0,0 @@
-# 人机协同(HITL)最佳实践
-
-[English](hitl-best-practices_en.md)
-
-人机协同用于在 Agent 执行工具前做审批拦截。它适合控制高风险操作、保留审计痕迹,并在人工审计压力过大时让审计 Agent 接管常规审批。
-
-## 配置入口
-
-Web 端进入 **系统设置 → 人机协同**,可配置:
-
-- 全局默认审批方:`human` 或 `audit_agent`
-- 审计 Agent 专用模型:`hitl.audit_model`
-- 已决策审计日志保留天数
-- 免审批工具白名单:`hitl.tool_whitelist`
-- 审批模式与审查编辑模式的审计提示词
-
-对应的 `config.yaml` 示例:
-
-```yaml
-hitl:
- default_reviewer: human
- audit_model:
- provider: ""
- base_url: ""
- api_key: ""
- model: "" # 可填小模型;留空复用 openai.model
- retention_days: 90
- tool_whitelist: [read_file, list_dir, glob, grep, tool_search]
-```
-
-`audit_model` 的字段可以只填一部分。空字段会自动继承主 `openai` 配置,因此常见做法是只填 `model`,让审计 Agent 使用更便宜的小模型。
-
-## 推荐审批策略
-
-### 1. 默认人工,逐步放权
-
-刚开始建议:
-
-- `default_reviewer: human`
-- 仅把明显只读工具加入 `tool_whitelist`
-- 对写文件、执行命令、C2 任务、WebShell 操作保持人工审批
-
-运行一段时间后,观察审计日志,把重复、低风险、误报少的工具加入白名单。
-
-### 2. 人工审不过来时,用小模型接管常规审批
-
-当待审批积压明显时,可以切换为:
-
-```yaml
-hitl:
- default_reviewer: audit_agent
- audit_model:
- model: "your-small-reviewer-model"
-```
-
-建议让小模型处理:
-
-- 只读查询
-- 信息收集
-- 端口与服务扫描
-- 目录枚举
-- 无破坏性的验证命令
-
-仍建议人工处理:
-
-- 删除、覆盖、清空数据
-- 修改权限、密码、账号
-- 持久化、横向移动、C2 高风险任务
-- 对生产目标的写入操作
-
-### 3. 用提示词定义组织策略
-
-审计 Agent 的提示词应该写成策略,而不是泛泛地说“谨慎审批”。建议明确:
-
-- 默认放行哪些低风险操作
-- 必须拒绝哪些破坏性操作
-- 哪些情况需要人工升级
-- 审查编辑模式下允许怎样收窄参数
-
-示例策略片段:
-
-```text
-常规信息收集、只读查询、端口扫描默认 approve。
-涉及删除文件、清空数据库、修改账号权限、写入持久化后门、停止关键服务时必须 reject。
-若目标范围超出用户授权范围,应 reject。
-审查编辑模式下,可将路径、目标、命令参数收窄后 approve,但不得扩大攻击面。
-```
-
-### 4. 白名单只放稳定低风险工具
-
-白名单工具会跳过审批,因此要保守维护。推荐放:
-
-- `read_file`
-- `list_dir`
-- `glob`
-- `grep`
-- `tool_search`
-
-不建议直接全局白名单:
-
-- 任意 shell 执行工具
-- 文件写入/删除工具
-- C2 任务工具
-- WebShell 命令执行工具
-
-## 模式选择
-
-| 模式 | 适用场景 |
-|------|----------|
-| 关闭 | 本地实验、完全信任工具链 |
-| 审批模式 | 只需要通过/拒绝 |
-| 审查编辑 | 希望审计 Agent 收窄参数后放行 |
-
-如果你已经配置了小模型审计,推荐从 **审批模式** 开始。只有当你希望 AI 自动收窄路径、目标范围或命令参数时,再开启 **审查编辑**。
-
-## 运维建议
-
-- 定期查看 **人机协同 → 审计日志**,调整白名单和提示词。
-- 高风险环境下保持 `default_reviewer: human`,只让审计 Agent 辅助给出建议。
-- 小模型审批失败时默认保守拒绝,这是预期行为。
-- 修改 `hitl.audit_model` 后先在页面点击 **测试审计模型**。
-- 对生产、客户、真实业务系统操作前,应保留人工最终确认。
diff --git a/docs/hitl-best-practices_en.md b/docs/hitl-best-practices_en.md
deleted file mode 100644
index 5f64574c..00000000
--- a/docs/hitl-best-practices_en.md
+++ /dev/null
@@ -1,122 +0,0 @@
-# Human-in-the-loop (HITL) Best Practices
-
-[中文](hitl-best-practices.md)
-
-HITL reviews tool calls before an Agent executes them. Use it to control high-risk operations, keep an audit trail, and let an Audit Agent take over routine approvals when human reviewers cannot keep up.
-
-## Where To Configure
-
-Open **System Settings → Human-in-the-loop** in the web UI. You can configure:
-
-- Global default reviewer: `human` or `audit_agent`
-- Dedicated Audit Agent model: `hitl.audit_model`
-- Resolved audit log retention days
-- No-approval tool allowlist: `hitl.tool_whitelist`
-- Audit prompts for approval mode and review-edit mode
-
-Example `config.yaml`:
-
-```yaml
-hitl:
- default_reviewer: human
- audit_model:
- provider: ""
- base_url: ""
- api_key: ""
- model: "" # set a small model here; blank reuses openai.model
- retention_days: 90
- tool_whitelist: [read_file, list_dir, glob, grep, tool_search]
-```
-
-`audit_model` supports partial configuration. Empty fields inherit from the main `openai` config, so the common setup is to fill only `model` and run approvals on a cheaper small model.
-
-## Recommended Approval Strategy
-
-### 1. Start With Humans, Then Delegate Gradually
-
-At the beginning, prefer:
-
-- `default_reviewer: human`
-- Only clearly read-only tools in `tool_whitelist`
-- Human approval for file writes, command execution, C2 tasks, and WebShell operations
-
-After observing audit logs, move repeated low-risk operations into the allowlist.
-
-### 2. Use A Small Model When Humans Cannot Keep Up
-
-When pending approvals start piling up, switch routine review to the Audit Agent:
-
-```yaml
-hitl:
- default_reviewer: audit_agent
- audit_model:
- model: "your-small-reviewer-model"
-```
-
-Good candidates for small-model review:
-
-- Read-only queries
-- Reconnaissance
-- Port and service scans
-- Directory enumeration
-- Non-destructive validation commands
-
-Keep human review for:
-
-- Deleting, overwriting, or clearing data
-- Modifying permissions, passwords, or accounts
-- Persistence, lateral movement, and high-risk C2 tasks
-- Writes against production targets
-
-### 3. Encode Your Policy In The Prompt
-
-The Audit Agent prompt should describe an operational policy, not just say “be careful.” Make it explicit:
-
-- Which low-risk actions are normally approved
-- Which destructive actions must be rejected
-- Which cases require escalation to a human
-- How review-edit mode may narrow arguments
-
-Example policy snippet:
-
-```text
-Approve routine reconnaissance, read-only queries, and port scans by default.
-Reject file deletion, database clearing, account or permission changes, persistence, and stopping critical services.
-Reject actions outside the user-authorized target scope.
-In review-edit mode, you may narrow paths, targets, or command arguments before approving, but must not expand the attack surface.
-```
-
-### 4. Keep The Allowlist Conservative
-
-Allowlisted tools skip approval, so keep the list stable and low-risk. Recommended examples:
-
-- `read_file`
-- `list_dir`
-- `glob`
-- `grep`
-- `tool_search`
-
-Avoid globally allowlisting:
-
-- Arbitrary shell execution tools
-- File write/delete tools
-- C2 task tools
-- WebShell command execution tools
-
-## Mode Selection
-
-| Mode | Best for |
-|------|----------|
-| Off | Local labs or fully trusted toolchains |
-| Approval | Approve/reject only |
-| Review-edit | Let the Audit Agent narrow arguments before approval |
-
-If you configured a small audit model, start with **Approval** mode. Use **Review-edit** only when you want the AI to safely narrow paths, target ranges, or command arguments.
-
-## Operations Tips
-
-- Review **Human-in-the-loop → Audit logs** regularly and tune allowlists/prompts.
-- In high-risk environments, keep `default_reviewer: human` and use the Audit Agent only for recommendations.
-- If the small-model reviewer fails, CyberStrikeAI rejects conservatively by default.
-- After changing `hitl.audit_model`, click **Test audit model** in the settings page.
-- For production, customer, or real business systems, keep a human as the final approver.
diff --git a/docs/robot.md b/docs/robot.md
deleted file mode 100644
index 0ce35959..00000000
--- a/docs/robot.md
+++ /dev/null
@@ -1,491 +0,0 @@
-# CyberStrikeAI 机器人使用说明
-
-[English](robot_en.md)
-
-本文档说明如何通过**个人微信**、**钉钉**、**飞书**与 **企业微信** 与 CyberStrikeAI 对话(长连接 / 回调模式),在手机端即可使用,无需在服务器上打开网页。按下面步骤操作可避免常见弯路。
-
----
-
-## 一、在 CyberStrikeAI 里从哪里配置
-
-1. 登录 CyberStrikeAI Web 端
-2. 左侧导航进入 **系统设置**
-3. 在左侧设置分类中点击 **机器人设置**(位于「基本设置」与「安全设置」之间)
-4. 按平台配置:
- - **个人微信**:点击「微信 / iLink」→「生成二维码并绑定」,用微信扫码确认(见 [3.4 个人微信](#34-个人微信-wechat--ilink))
- - **钉钉**:勾选并填写 Client ID / Client Secret
- - **飞书**:勾选并填写 App ID / App Secret
-5. 点击 **应用配置** 保存(微信扫码绑定成功后会**自动保存并启用**,一般无需再点)
-6. **重启 CyberStrikeAI 应用**(钉钉/飞书:只保存不重启,长连接不会建立;微信绑定成功后会自动重启连接,通常无需手动重启)
-
-配置会写入 `config.yaml` 的 `robots` 段,也可在配置文件中直接编辑。**修改钉钉/飞书配置后必须重启,长连接才会生效。** 个人微信绑定成功后程序会自动写入 `robots.wechat` 并重启 iLink 长轮询。
-
----
-
-## 二、支持的平台(长连接 / 回调)
-
-| 平台 | 说明 |
-|----------|------|
-| 个人微信 | 使用微信 iLink 协议,Web 端扫码绑定后长轮询收消息,**无需公网回调** |
-| 钉钉 | 使用 Stream 长连接,程序主动连接钉钉接收消息 |
-| 飞书 | 使用长连接,程序主动连接飞书接收消息 |
-| 企业微信 | 使用 HTTP 回调接收消息,被动回包 + 主动调用企业微信发送消息 API |
-| Telegram | Bot API 长轮询(getUpdates),**无需公网回调** |
-| Slack | Socket Mode(出站 WebSocket),**无需公网回调** |
-| Discord | Gateway WebSocket,**无需公网回调** |
-| QQ 机器人 | QQ 开放平台 WebSocket(C2C / 群 @),**无需公网回调** |
-
-下面第三节会按平台写清:在开放平台要做什么、要复制哪些字段、填到 CyberStrikeAI 的哪一栏。
-
----
-
-## 三、各平台配置项与详细步骤
-
-### 3.1 钉钉
-
-**先搞清楚:两种钉钉机器人不一样**
-
-| 类型 | 从哪里创建 | 能否做「用户发消息→机器人回复」 | 本程序是否支持 |
-|------|------------|----------------------------------|----------------|
-| **自定义机器人** | 钉钉群里:群设置 → 添加机器人 → 自定义(Webhook) | ❌ 不能,只能你往群里发消息 | ❌ 不支持 |
-| **企业内部应用机器人** | [钉钉开放平台](https://open.dingtalk.com) 创建应用并开通机器人 | ✅ 能 | ✅ 支持 |
-
-如果你手里是「自定义机器人」的 Webhook 地址(`oapi.dingtalk.com/robot/send?access_token=xxx`)和加签密钥(`SEC...`),**不能直接填到本程序**,必须按下面步骤在开放平台创建「企业内部应用」并拿到 **Client ID**、**Client Secret**。
-
----
-
-**钉钉配置完整步骤(按顺序做)**
-
-1. **打开钉钉开放平台**
- 浏览器访问 [https://open.dingtalk.com](https://open.dingtalk.com),用**企业管理员**账号登录。
-
-2. **进入应用开发**
- 左侧选 **应用开发** → **企业内部开发** → 点击 **创建应用**(或选择已有应用)。填写应用名称等基本信息后创建。
-
-3. **拿到 Client ID 和 Client Secret**
- - 左侧点 **凭证与基础信息**(在「基础信息」下)。
- - 页面上有 **Client ID(原 AppKey)** 和 **Client Secret(原 AppSecret)**。
- - 点击复制,**不要手打**,注意:数字 **0** 和字母 **o**、数字 **1** 和字母 **l** 容易抄错(例如 `ding9gf9tiozuc504aer` 中间是数字 **504** 不是 5o4)。
-
-4. **开通机器人并选 Stream 模式**
- - 左侧 **应用能力** → **机器人**。
- - 打开「机器人配置」开关。
- - 填写机器人名称、简介等(必填项按提示填)。
- - **关键**:消息接收方式要选 **「Stream 模式」**(流式接入)。若只有「HTTP 回调」或未选 Stream,本程序收不到消息。
- - 保存。
-
-5. **权限与发布**
- - 左侧 **权限管理**:搜索「机器人」「消息」等,勾选**接收消息**、**发送消息**等机器人相关权限,并确认授权。
- - 左侧 **版本管理与发布**:若有未发布配置,点击 **发布新版本** / **上线**,否则修改不生效。
-
-6. **填回 CyberStrikeAI**
- - 回到 CyberStrikeAI → 系统设置 → 机器人设置 → 钉钉。
- - 勾选「启用钉钉机器人」。
- - **Client ID (AppKey)** 粘贴第 3 步复制的 Client ID。
- - **Client Secret** 粘贴第 3 步复制的 Client Secret。
- - 点击 **应用配置**,然后**重启 CyberStrikeAI**。
-
----
-
-**CyberStrikeAI 钉钉栏位对照**
-
-| CyberStrikeAI 中填写项 | 在钉钉开放平台的来源 |
-|------------------------|------------------------|
-| 启用钉钉机器人 | 勾选即启用 |
-| Client ID (AppKey) | 凭证与基础信息 → **Client ID(原 AppKey)** |
-| Client Secret | 凭证与基础信息 → **Client Secret(原 AppSecret)** |
-
----
-
-### 3.2 飞书 (Lark)
-
-| 配置项 | 说明 |
-|--------|------|
-| 启用飞书机器人 | 勾选后启动飞书长连接 |
-| App ID | 飞书开放平台应用凭证中的 App ID |
-| App Secret | 飞书开放平台应用凭证中的 App Secret |
-| Verify Token | 事件订阅用(可选) |
-
-**飞书配置简要步骤**:登录 [飞书开放平台](https://open.feishu.cn) → 创建企业自建应用 → 在「凭证与基础信息」中获取 **App ID**、**App Secret** → 在「应用能力」中开通**机器人**并启用相应权限 → **在「事件订阅」中添加事件**(见下)→ 发布应用 → 将 App ID、App Secret 填到 CyberStrikeAI 机器人设置 → 保存。
-
-**重要:事件订阅**
-飞书长连接只有在开放平台订阅了「接收消息」事件后才会收到用户消息。请在该应用的 **事件订阅** 页面点击「添加事件」,在「消息与群组」下勾选 **接收消息(im.message.receive_v1)** 或同类事件;若未添加,连接会建立成功但收不到任何消息,表现为发消息后本地无日志、机器人无回复。
-
-**飞书权限配置(必读)**
-在 **权限管理** 中需开通以下权限(与开放平台列表中的名称、标识一致);修改后需在 **版本管理与发布** 中发布新版本才生效。
-
-| 权限名称(开放平台中显示) | 权限标识 | 说明 |
-|----------------------------|----------|------|
-| 获取与发送单聊、群组消息 | `im:message` | 收发消息的基础权限,**必须开通**。 |
-| 接收群聊中@机器人消息事件 | `im:message.group_at_msg:readonly` | 群聊中 @ 机器人时收消息,需开通。 |
-| 读取用户发给机器人的单聊消息 | `im:message.p2p_msg:readonly` | 单聊收消息,**必须开通**,否则私聊发消息没反应。 |
-| 获取单聊、群组消息 | `im:message:readonly` | 读取消息内容,**必须开通**。 |
-
-**事件订阅**(与权限分开配置):在 **事件订阅** 中添加 **接收消息(im.message.receive_v1)**,否则长连接收不到消息推送。
-
-- **单聊**:在飞书里打开与机器人的私聊窗口,直接发「帮助」或任意文字即可,无需 @。
-- **群聊**:在群里只有 **@ 机器人** 后发送的内容才会被机器人收到并回复。
-
----
-
-### 3.3 企业微信 (WeCom)
-
-> 企业微信目前采用「HTTP 回调 + 主动发送消息 API」的方式工作:
-> - 用户发消息 → 企业微信以加密 XML **回调到你的服务器**(本程序的 `/api/robot/wecom`);
-> - CyberStrikeAI 解密并调用 AI → 使用企业微信的 `message/send` 接口**主动发消息给用户**。
-
-**配置概览:**
-
-- 在企业微信管理后台创建或选择一个**自建应用**。
-- 在该应用的「接收消息」处配置回调 URL、Token、EncodingAESKey。
-- 在 CyberStrikeAI 的 `config.yaml` 中填入:
- - `robots.wecom.corp_id`:企业 ID(CorpID)
- - `robots.wecom.agent_id`:应用的 AgentId
- - `robots.wecom.token`:消息回调使用的 Token
- - `robots.wecom.encoding_aes_key`:消息回调使用的 EncodingAESKey
- - `robots.wecom.secret`:该应用的 Secret(用于调用企业微信主动发送消息接口)
-
-> **重要:IP 白名单(errcode 60020)**
-> CyberStrikeAI 使用 `https://qyapi.weixin.qq.com/cgi-bin/message/send` 主动发送 AI 回复。
-> 若企业微信日志或本程序日志中出现 `errcode 60020 not allow to access from your ip`:
->
-> - 说明你的服务器出口 IP **没有加入企业微信的 IP 白名单**;
-> - 请在企业微信管理后台中找到该自建应用的**「安全设置 / IP 白名单」**(具体入口可能因版本略有不同),将运行 CyberStrikeAI 的服务器公网 IP(如 `110.xxx.xxx.xxx`)加入白名单;
-> - 保存后等待生效,再次发送消息测试。
->
-> 如果 IP 未加入白名单,企业微信会拒绝主动发送消息,表现为:
-> - 回调接口 `/api/robot/wecom` 能正常收到并处理消息;
-> - 但手机端**始终收不到 AI 回复**,日志中有 `not allow to access from your ip` 提示。
-
----
-
-### 3.4 个人微信 (WeChat / iLink)
-
-> 个人微信采用「Web 扫码绑定 + iLink 长轮询」方式工作:
-> - 在 CyberStrikeAI Web 端生成二维码 → 用**手机微信**扫码并确认绑定;
-> - 绑定成功后自动写入 `config.yaml` 的 `robots.wechat`,并启动 iLink 长轮询(程序主动连接 `ilinkai.weixin.qq.com` 收消息);
-> - **无需**在服务器上配置公网回调 URL,也**无需**去微信开放平台注册应用。
-
-**与企业微信的区别**
-
-| 项目 | 个人微信 (iLink) | 企业微信 (WeCom) |
-|------|------------------|------------------|
-| 使用场景 | 个人微信私聊 | 企业微信自建应用 |
-| 配置方式 | Web 端扫码绑定 | 管理后台配置回调 URL + Token |
-| 是否需要公网 | 否(长轮询出站即可) | 是(需可被企业微信访问的 HTTPS 回调) |
-| 配置段 | `robots.wechat` | `robots.wecom` |
-
-**绑定步骤(按顺序做)**
-
-1. **登录 CyberStrikeAI Web 端**
- 左侧 **系统设置** → **机器人设置** → 点击 **微信 / iLink** 卡片。
-
-2. **(可选)勾选「启用微信机器人」**
- 首次绑定可跳过;绑定成功后会自动勾选并启用。
-
-3. **生成二维码**
- 点击 **「生成二维码并绑定」**。页面会显示二维码(约 **5 分钟**有效;过期请重新生成)。
-
-4. **微信扫码确认**
- - 用手机微信扫描页面二维码;
- - 按手机提示完成确认;
- - 若手机微信弹出**配对数字**,在 Web 页面对应输入框填写并点击 **提交**(仅部分账号需要)。
-
-5. **等待绑定完成**
- 页面显示「绑定成功,微信机器人已启用」即完成。`bot_token`、`ilink_bot_id` 等会自动写入 `config.yaml`,程序会自动重启 iLink 长轮询,**一般无需手动重启服务**。
-
-6. **在手机微信里测试**
- 打开与 CyberStrikeAI 机器人的**私聊**(绑定后微信内会出现对应会话),发送「帮助」或任意文字测试。
-
-**CyberStrikeAI 微信栏位说明**
-
-| 栏位 | 说明 |
-|------|------|
-| 启用微信机器人 | 勾选后启动 iLink 长轮询;绑定成功后会自动勾选 |
-| 生成二维码并绑定 | 发起扫码绑定流程 |
-| **高级设置**(一般保持默认即可) | |
-| API Base URL | 默认 `https://ilinkai.weixin.qq.com` |
-| Bot Type | 默认 `3` |
-| Bot Agent | 默认 `CyberStrikeAI/1.0` |
-| iLink Bot ID | 绑定成功后自动填充,只读 |
-
-**使用方式**
-
-- 仅支持在与机器人的**私聊**中对话,直接发送文字即可,**不需要 @**。
-- 不支持群聊 @ 机器人(与钉钉/飞书群聊不同)。
-- 仅处理**文本消息**;图片、语音等会忽略或提示暂不支持。
-
-**重新绑定**
-
-- 若需更换绑定的微信账号,在机器人设置页点击 **「重新绑定」**,再次扫码即可。
-- 若提示「该微信已绑定过,无需重复绑定」,说明该账号此前已完成绑定。
-
-**常见问题**
-
-| 现象 | 处理 |
-|------|------|
-| 二维码过期 | 重新点击「生成二维码并绑定」(有效期约 5 分钟) |
-| 扫码后要求输入数字 | 查看手机微信显示的配对数字,在 Web 页面输入并提交 |
-| 绑定成功但发消息无回复 | 看程序日志是否有 `微信 iLink 长轮询已启动`、`微信收到消息`;确认已勾选「启用微信机器人」 |
-| 断网或睡眠后无回复 | 程序会自动重连(约 5~60 秒);仍无回复可重启 CyberStrikeAI |
-| 无法生成二维码 | 确认服务器能访问 `https://ilinkai.weixin.qq.com`(出站 HTTPS) |
-
----
-
-### 3.5 Telegram
-
-> Telegram 使用 **Bot API 长轮询**(`getUpdates`):程序主动连接 `api.telegram.org` 收消息,**无需公网回调**。
-
-**配置步骤:**
-
-1. 在 Telegram 中找 **@BotFather**,发送 `/newbot` 创建机器人,获得 **Bot Token**。
-2. CyberStrikeAI → **系统设置** → **机器人设置** → **Telegram**。
-3. 勾选「启用 Telegram 机器人」,粘贴 **Bot Token**。
-4. (可选)填写 Bot Username(不含 `@`),或留空由程序自动 `getMe`。
-5. (可选)勾选「允许群聊」— 群聊中仅响应 **@机器人** 的消息。
-6. 点击 **应用配置**(会自动重启长轮询连接)。
-
-**使用:** 与机器人私聊直接发消息;群聊需 @ 机器人(且已勾选允许群聊)。
-
----
-
-### 3.6 Slack
-
-> Slack 使用 **Socket Mode**(出站 WebSocket):需 **Bot Token** 与 **App-Level Token**,**无需公网回调**。
-
-**配置步骤:**
-
-1. 在 [Slack API](https://api.slack.com/apps) 创建 App → 启用 **Socket Mode**。
-2. **Basic Information** → **App-Level Tokens** → 创建 token(scope: `connections:write`),即 **xapp-** 开头。
-3. **OAuth & Permissions** → 添加 Bot Token Scopes:`app_mentions:read`、`chat:write`、`im:history`、`im:read` 等 → 安装到工作区,获得 **xoxb-** Bot Token。
-4. **Event Subscriptions** → 订阅 `message.im`、`app_mention` 等(Socket Mode 下在应用内配置)。
-5. 在 CyberStrikeAI 填入 Bot Token 与 App-Level Token → **应用配置**。
-
-**使用:** 与 Bot 私聊直接发;频道中需 @ 机器人。
-
----
-
-### 3.7 Discord
-
-> Discord 使用 **Gateway WebSocket**:程序主动连接 Discord Gateway,**无需公网回调**。
-
-**配置步骤:**
-
-1. 在 [Discord Developer Portal](https://discord.com/developers/applications) 创建应用 → **Bot** → 复制 **Token**。
-2. 开启 **Privileged Gateway Intents** 中的 **Message Content Intent**(否则读不到消息正文)。
-3. OAuth2 → URL Generator → scopes: `bot` → 权限勾选 **Send Messages**、**Read Message History** 等 → 邀请 Bot 到服务器。
-4. CyberStrikeAI → **机器人设置** → **Discord** → 填入 Token → **应用配置**。
-5. (可选)勾选「允许服务器频道」— 频道中仅响应 **@机器人**。
-
-**使用:** 与 Bot 私聊直接发;服务器频道需 @ 机器人(且已勾选允许服务器频道)。
-
----
-
-### 3.8 QQ 机器人
-
-> QQ 机器人使用 **QQ 开放平台 WebSocket**(官方 `botgo` SDK):支持 C2C 私聊与群 @,**无需公网回调**(WebSocket 出站连接)。
-
-**配置步骤:**
-
-1. 在 [QQ 机器人开放平台](https://q.qq.com) 创建机器人,获取 **App ID** 与 **Client Secret**。
-2. 在沙箱中添加测试成员(上线前仅沙箱可对话)。
-3. 订阅 **C2C 消息**、**群 @ 消息** 等事件(WebSocket 模式)。
-4. CyberStrikeAI → **机器人设置** → **QQ 机器人** → 填入 App ID、Client Secret。
-5. 测试阶段勾选 **沙箱环境**;正式上线后取消沙箱并发布。
-6. 点击 **应用配置**。
-
-**使用:** 与机器人 C2C 私聊直接发;QQ 群中需 @ 机器人。
-
-> 注意:QQ 官方正逐步推广 Webhook 回调;当前实现使用 WebSocket(与钉钉/飞书类似的长连接模式)。若配置变更后连接未刷新,可重启 CyberStrikeAI 进程。
-
----
-
-## 四、机器人命令
-
-在任一已接入平台(钉钉/飞书/微信/Telegram/Slack/Discord/QQ 等)向机器人发送以下**文本命令**(仅支持文本):
-
-| 命令 | 说明 |
-|------|------|
-| **帮助** | 显示命令帮助与说明 |
-| **列表** 或 **对话列表** | 列出所有对话的标题与对话 ID |
-| **切换 \<对话ID\>** 或 **继续 \<对话ID\>** | 指定对话 ID,后续消息在该对话中继续 |
-| **新对话** | 开启一个新对话,后续消息在新对话中 |
-| **清空** | 清空当前对话上下文(效果等同「新对话」) |
-| **当前** | 显示当前对话 ID 与标题 |
-| **停止** | 中断当前正在执行的任务 |
-| **角色** 或 **角色列表** | 列出所有可用角色(渗透测试、CTF、Web 应用扫描等) |
-| **角色 \<角色名\>** 或 **切换角色 \<角色名\>** | 切换当前使用的角色 |
-| **删除 \<对话ID\>** | 删除指定对话 |
-| **版本** | 显示当前 CyberStrikeAI 版本号 |
-
-除以上命令外,**直接输入任意文字**会作为用户消息发给 AI,与 Web 端对话逻辑一致(渗透测试/安全分析等)。
-
----
-
-## 五、如何使用(要 @ 机器人吗?)
-
-- **个人微信**:在与 CyberStrikeAI 机器人的**私聊**中直接发送即可,**不需要 @**(不支持群聊)。
-- **钉钉 / 飞书单聊(推荐)**:**搜索并打开该机器人**,进入**私聊**,直接输入「帮助」或任意文字即可,**不需要 @**。
-- **钉钉 / 飞书群聊**:若机器人被添加到群里,在群内只有 **@机器人** 后发送的消息才会被机器人收到并回复;不 @ 的群消息不会触发机器人。
-
-总结:**个人微信、单聊时直接发**;**钉钉/飞书在群里用时需要 @机器人** 再发内容。
-
----
-
-## 六、推荐使用流程(避免漏步骤)
-
-**个人微信(最简单,无需开放平台)**
-
-1. CyberStrikeAI Web 端 → 系统设置 → 机器人设置 → **微信 / iLink** → **生成二维码并绑定**。
-2. 手机微信扫码确认(如需配对数字则在 Web 页填写)。
-3. 绑定成功后,在手机微信私聊中发「帮助」测试。
-
-**钉钉 / 飞书**
-
-1. **在开放平台**:按第三节完成应用创建、凭证复制、机器人开通(钉钉务必选 **Stream 模式**)、权限与发布。
-2. **在 CyberStrikeAI**:系统设置 → 机器人设置 → 勾选对应平台,粘贴 Client ID/App ID、Client Secret/App Secret → 点击 **应用配置**。
-3. **重启 CyberStrikeAI 进程**(否则长连接不会建立)。
-4. **在手机钉钉/飞书**:找到该机器人(单聊直接发,群聊需 @机器人),发「帮助」或任意内容测试。
-
-若发消息没反应,先看 **第九节排查** 和 **第十节常见弯路**。
-
----
-
-## 七、配置文件示例
-
-`config.yaml` 中机器人相关片段示例:
-
-```yaml
-robots:
- wechat: # 个人微信 iLink(扫码绑定后自动写入,一般无需手填)
- enabled: true
- bot_token: "your_bot_token@im.bot:..."
- ilink_bot_id: "your_bot_id@im.bot"
- ilink_user_id: "your_user_id@im.wechat"
- base_url: "https://ilinkai.weixin.qq.com"
- bot_type: "3"
- bot_agent: "CyberStrikeAI/1.0"
- dingtalk:
- enabled: true
- client_id: "your_dingtalk_app_key"
- client_secret: "your_dingtalk_app_secret"
- lark:
- enabled: true
- app_id: "your_lark_app_id"
- app_secret: "your_lark_app_secret"
- verify_token: ""
- wecom:
- enabled: false
- corp_id: ""
- agent_id: 0
- token: ""
- encoding_aes_key: ""
- secret: ""
- telegram:
- enabled: false
- bot_token: ""
- allow_group_messages: false
- slack:
- enabled: false
- bot_token: ""
- app_token: ""
- discord:
- enabled: false
- bot_token: ""
- allow_guild_messages: false
- qq:
- enabled: false
- app_id: ""
- client_secret: ""
- sandbox: true
-```
-
-修改钉钉/飞书/企业微信/Telegram/Slack/Discord/QQ 配置后,点击 **应用配置** 会自动重启对应长连接。个人微信扫码绑定成功后会自动写入并重启 iLink 连接。
-
----
-
-## 八、如何验证是否可用(无需钉钉/飞书客户端)
-
-在未安装钉钉或飞书时,可用**测试接口**验证机器人逻辑是否正常:
-
-1. 先登录 CyberStrikeAI Web 端(保证有登录态)。
-2. 使用 curl 调用测试接口(需携带登录后的 Cookie):
-
-```bash
-# 将 YOUR_COOKIE 替换为登录后获得的 Cookie(浏览器 F12 → 网络 → 任意请求 → 请求头中的 Cookie)
-curl -X POST "http://localhost:8080/api/robot/test" \
- -H "Content-Type: application/json" \
- -H "Cookie: YOUR_COOKIE" \
- -d '{"platform":"dingtalk","user_id":"test_user","text":"帮助"}'
-```
-
-若返回 JSON 中含有 `"reply":"【CyberStrikeAI 机器人命令】..."`,说明命令处理正常。可再试 `"text":"列表"`、`"text":"当前"` 等。
-
-接口说明:`POST /api/robot/test`(需登录),请求体 `{"platform":"可选","user_id":"可选","text":"必填"}`,响应 `{"reply":"回复内容"}`。
-
----
-
-## 九、发消息没反应时排查
-
-### 9.1 个人微信
-
-按顺序检查:
-
-1. **是否已完成扫码绑定**
- 机器人设置页应显示「已连接」或已绑定 Bot ID;`config.yaml` 中 `robots.wechat.bot_token` 不应为空。
-
-2. **是否已启用**
- 确认「启用微信机器人」已勾选;若刚修改过,可重启 CyberStrikeAI 进程。
-
-3. **看程序日志**
- - 启动后应看到:`微信 iLink 长轮询已启动`;
- - 发消息后应有:`微信收到消息`;若没有,多为未绑定成功或 `bot_token` 失效,可尝试 **重新绑定**。
- - 若出现 `微信 iLink 长轮询异常,将自动重连`,等待自动重连或重启进程。
-
-4. **网络**
- 服务器需能访问 `https://ilinkai.weixin.qq.com`(出站 HTTPS)。绑定阶段若无法生成二维码,优先检查此项。
-
-5. **断网或睡眠后**
- 与钉钉/飞书类似,程序会**自动重连**(约 5~60 秒);仍无回复可重启 CyberStrikeAI。
-
-### 9.2 钉钉
-
-按顺序检查:
-
-0. **笔记本合盖睡眠 / 断网后**
- 钉钉、飞书均使用长连接收消息,睡眠或断网后连接会断开。程序会**自动重连**(约 5 秒~60 秒内重试)。唤醒或恢复网络后稍等一会儿再发消息;若仍无反应,可重启 CyberStrikeAI 进程。
-
-1. **Client ID / Client Secret 是否与开放平台完全一致**
- 从「凭证与基础信息」里**复制粘贴**,不要手打。注意数字 **0** 与字母 **o**、数字 **1** 与字母 **l**(例如 `ding9gf9tiozuc504aer` 中间是 **504** 不是 5o4)。
-
-2. **是否在保存配置后重启了应用**
- 机器人长连接在**应用启动时**建立。在 Web 端点击「应用配置」只写入配置文件,**必须重启 CyberStrikeAI 进程**后钉钉连接才会生效。
-
-3. **看程序日志**
- - 启动后应看到:`钉钉 Stream 正在连接…`、`钉钉 Stream 已启动(无需公网),等待收消息`。
- - 若出现 `钉钉 Stream 长连接退出` 且带错误信息,多为 **Client ID / Client Secret 错误**或**开放平台未开通流式接入**。
- - 在钉钉里发一条消息后,若有收到,应有日志:`钉钉收到消息`;若没有,说明钉钉未把消息推到本程序(回头检查开放平台「机器人」是否开通、是否选用 **Stream 模式**)。
-
-4. **开放平台侧**
- 应用需已**发布**;在「机器人」能力中需开启**流式接入(Stream)** 用于接收消息(仅 HTTP 回调不够);权限管理里需有机器人接收、发送消息等权限。
-
----
-
-## 十、常见弯路(避免踩坑)
-
-- **个人微信与企业微信混淆**:个人微信走 `robots.wechat` + Web 扫码绑定;企业微信走 `robots.wecom` + 管理后台回调 URL,二者完全不同。
-- **个人微信二维码过期**:二维码约 5 分钟有效,过期需重新生成,不要一直扫旧码。
-- **用错了机器人类型**:在钉钉**群里**添加的「自定义」机器人(Webhook + 加签)**不能**用来做对话,本程序只支持**开放平台「企业内部应用」**里的机器人。
-- **只保存没重启**:钉钉/飞书改完配置后必须**重启应用**,否则长连接不会建立(个人微信扫码绑定会自动重启连接)。
-- **Client ID 抄错**:开放平台是 `504` 就填 `504`,不要填成 `5o4`;尽量用复制粘贴。
-- **钉钉只开了 HTTP 回调没开 Stream**:本程序通过 **Stream 长连接**收消息,开放平台里机器人的消息接收方式必须选 **Stream 模式**。
-- **应用没发布**:开放平台里修改了机器人或权限后,要在「版本管理与发布」里**发布新版本**,否则不生效。
-
----
-
-## 十一、注意事项
-
-- 各平台均**仅处理文本消息**;其他类型(如图片、语音)会提示暂不支持或忽略。
-- 个人微信仅支持**私聊**,不支持群聊 @ 机器人。
-- 会话与 Web 端共用同一套对话数据:在机器人里创建的对话会在 Web 端「对话」列表中看到,反之亦然。
-- 机器人执行与 **Eino 单/多代理** 相同逻辑(`ProcessMessageForRobot`,含进度回调与过程详情入库),仅不向客户端推送 SSE,最后一次性回复个人微信/钉钉/飞书/企业微信。默认 `robot_default_agent_mode: eino_single`。
diff --git a/docs/robot_en.md b/docs/robot_en.md
deleted file mode 100644
index 8284f896..00000000
--- a/docs/robot_en.md
+++ /dev/null
@@ -1,463 +0,0 @@
-# CyberStrikeAI Robot / Chatbot Guide
-
-[中文](robot.md)
-
-This document explains how to chat with CyberStrikeAI from **personal WeChat**, **DingTalk**, **Lark (Feishu)**, and **WeCom (Enterprise WeChat)** using long-lived connections or HTTP callbacks—no need to open a browser on the server. Following the steps below helps avoid common mistakes.
-
----
-
-## 1. Where to configure in CyberStrikeAI
-
-1. Log in to the CyberStrikeAI web UI.
-2. Open **System Settings** in the left sidebar.
-3. Click **Robot settings** (between “Basic” and “Security”).
-4. Configure per platform:
- - **Personal WeChat**: Open **WeChat / iLink** → **Generate QR code and bind**, then scan with WeChat (see [Section 3.4](#34-personal-wechat-wechat--ilink))
- - **DingTalk**: Enable and fill in Client ID / Client Secret
- - **Lark**: Enable and fill in App ID / App Secret
-5. Click **Apply configuration** to save (WeChat binding saves and enables automatically on success—usually no extra click needed)
-6. **Restart the CyberStrikeAI process** (DingTalk/Lark: saving alone does not establish the connection; WeChat auto-restarts the iLink poll after binding—usually no manual restart needed)
-
-Settings are written to the `robots` section of `config.yaml`; you can also edit the file directly. **After changing DingTalk or Lark config, you must restart for the long-lived connection to take effect.** Personal WeChat binding automatically writes `robots.wechat` and restarts the iLink long poll.
-
----
-
-## 2. Supported platforms (long-lived / callback)
-
-| Platform | Description |
-|----------------|-------------|
-| Personal WeChat| WeChat iLink protocol; scan QR in the web UI to bind, then long-poll for messages—**no public callback URL needed** |
-| DingTalk | Stream long-lived connection; the app connects to DingTalk to receive messages |
-| Lark (Feishu) | Long-lived connection; the app connects to Lark to receive messages |
-| WeCom (Qiye WX)| HTTP callback to receive messages; CyberStrikeAI replies via WeCom’s message sending API |
-| Telegram | Bot API long polling (`getUpdates`); **no public callback URL needed** |
-| Slack | Socket Mode (outbound WebSocket); **no public callback URL needed** |
-| Discord | Gateway WebSocket; **no public callback URL needed** |
-| QQ Bot | QQ Open Platform WebSocket (C2C / group @); **no public callback URL needed** |
-
-Section 3 below describes, per platform, what to do in the developer console and which fields to copy into CyberStrikeAI.
-
----
-
-## 3. Configuration and step-by-step setup
-
-### 3.1 DingTalk
-
-**Important: two types of DingTalk bots**
-
-| Type | Where it’s created | Can do “user sends message → bot replies”? | Supported here? |
-|------|-------------------|-------------------------------------------|------------------|
-| **Custom bot (Webhook)** | In a DingTalk group: Group settings → Add robot → Custom (Webhook) | No; you can only post to the group | No |
-| **Enterprise internal app bot** | [DingTalk Open Platform](https://open.dingtalk.com): create an app and enable the bot | Yes | Yes |
-
-If you only have a **custom bot** Webhook URL (`oapi.dingtalk.com/robot/send?access_token=...`) and sign secret (`SEC...`), **do not** put them into CyberStrikeAI. You must create an **enterprise internal app** in the open platform and obtain **Client ID** and **Client Secret** as below.
-
----
-
-**DingTalk setup (in order)**
-
-1. **Open DingTalk Open Platform**
- Go to [https://open.dingtalk.com](https://open.dingtalk.com) and log in with an **enterprise admin** account.
-
-2. **Create or select an app**
- In the left menu: **Application development** → **Enterprise internal development** → **Create application** (or choose an existing app). Fill in the app name and create.
-
-3. **Get Client ID and Client Secret**
- - In the left menu open **Credentials and basic info** (under “Basic information”).
- - Copy **Client ID (formerly AppKey)** and **Client Secret (formerly AppSecret)**.
- - Use copy/paste; avoid typing by hand. Watch for **0** vs **o** and **1** vs **l** (e.g. `ding9gf9tiozuc504aer` has the digits **504**, not 5o4).
-
-4. **Enable the bot and choose Stream mode**
- - Left menu: **Application capabilities** → **Robot**.
- - Turn on “Robot configuration”.
- - Fill in robot name, description, etc. as required.
- - **Critical**: set message reception to **“Stream mode”** (流式接入). If you only enable “HTTP callback” or do not select Stream, CyberStrikeAI will not receive messages.
- - Save.
-
-5. **Permissions and release**
- - Left menu: **Permission management** — search for “robot”, “message”, etc., and enable **receive message**, **send message**, and other bot-related permissions; confirm.
- - Left menu: **Version management and release** — if there are unpublished changes, click **Release new version** / **Publish**; otherwise changes do not take effect.
-
-6. **Fill in CyberStrikeAI**
- - In CyberStrikeAI: System settings → Robot settings → DingTalk.
- - Enable “Enable DingTalk robot”.
- - Paste the Client ID and Client Secret from step 3.
- - Click **Apply configuration**, then **restart CyberStrikeAI**.
-
----
-
-**Field mapping (DingTalk)**
-
-| Field in CyberStrikeAI | Source in DingTalk Open Platform |
-|------------------------|----------------------------------|
-| Enable DingTalk robot | Check to enable |
-| Client ID (AppKey) | Credentials and basic info → **Client ID (formerly AppKey)** |
-| Client Secret | Credentials and basic info → **Client Secret (formerly AppSecret)** |
-
----
-
-### 3.2 Lark (Feishu)
-
-| Field | Description |
-|-------|-------------|
-| Enable Lark robot | Check to start the Lark long-lived connection |
-| App ID | From Lark open platform app credentials |
-| App Secret | From Lark open platform app credentials |
-| Verify Token | Optional; for event subscription |
-
-**Lark setup in short**: Log in to [Lark Open Platform](https://open.feishu.cn) → Create an enterprise app → In “Credentials and basic info” get **App ID** and **App Secret** → In “Application capabilities” enable **Robot** and the right permissions → Add **event subscription** and **permissions** below → Publish the app → Enter App ID and App Secret in CyberStrikeAI robot settings → Save and **restart** the app.
-
-**Event subscription**
-The long-lived connection only receives message events if you subscribe to them. In the app’s **Events and callbacks** (事件与回调) → **Event subscription** (事件订阅), add the event **Receive message** (**im.message.receive_v1**). Without it, the connection succeeds but no message events are delivered (no logs when users send messages).
-
-**Lark permissions (required)**
-In **Permission management** (权限管理), enable the following (names and identifiers match the Lark console). After changes, **publish a new version** in Version management and release so they take effect.
-
-| Permission name (as shown in console) | Identifier | Notes |
-|--------------------------------------|------------|-------|
-| 获取与发送单聊、群组消息 (Get and send direct & group messages) | `im:message` | Base permission for sending and receiving; **required**. |
-| 接收群聊中@机器人消息事件 (Receive @bot messages in group chat) | `im:message.group_at_msg:readonly` | Required for group chat when users @ the bot. |
-| 读取用户发给机器人的单聊消息 (Read direct messages from users to bot) | `im:message.p2p_msg:readonly` | **Required** for 1:1 chat; otherwise no response in private chat. |
-| 获取单聊、群组消息 (Get direct & group messages) | `im:message:readonly` | **Required** to read message content. |
-
-**Event subscription** (configured separately): In **Event subscription** (事件订阅), add **Receive message** (**im.message.receive_v1**). Without it, the long-lived connection will not receive message events.
-
-- **1:1 chat**: Open the bot’s private chat in Lark and send e.g. “帮助” or “help”; no @ needed.
-- **Group chat**: Only messages that **@ the bot** are received and replied to.
-
----
-
-### 3.3 WeCom (Enterprise WeChat)
-
-> WeCom uses a **“HTTP callback + active message send API”** model:
-> - User sends a message → WeCom sends an **encrypted XML callback** to your server (CyberStrikeAI’s `/api/robot/wecom`).
-> - CyberStrikeAI decrypts it, calls the AI, then uses WeCom’s `message/send` API to **actively push the reply** to the user.
-
-**Configuration overview:**
-
-- In the WeCom admin console, create or select a **custom app** (自建应用).
-- In that app’s settings, configure the message **callback URL**, **Token**, and **EncodingAESKey**.
-- In CyberStrikeAI’s `config.yaml`, fill in:
- - `robots.wecom.corp_id`: your CorpID (企业 ID)
- - `robots.wecom.agent_id`: the app’s AgentId
- - `robots.wecom.token`: the Token used for message callbacks
- - `robots.wecom.encoding_aes_key`: the EncodingAESKey used for callbacks
- - `robots.wecom.secret`: the app’s Secret (used when calling WeCom APIs to send messages)
-
-> **Important: IP allowlist (errcode 60020)**
-> CyberStrikeAI calls `https://qyapi.weixin.qq.com/cgi-bin/message/send` to actively send AI replies.
-> If logs show `errcode 60020 not allow to access from your ip`:
->
-> - Your server’s outbound IP is **not in WeCom’s IP allowlist**.
-> - In the WeCom admin console, open the custom app’s **Security / IP allowlist** settings (name may vary slightly), and add the public IP of the machine running CyberStrikeAI (e.g. `110.xxx.xxx.xxx`).
-> - Save and wait for it to take effect, then test again.
->
-> If the IP is not whitelisted, WeCom will reject active message sending. You will see that `/api/robot/wecom` receives and processes callbacks, but users **never see AI replies**, and logs contain `not allow to access from your ip`.
-
----
-
-### 3.4 Personal WeChat (WeChat / iLink)
-
-> Personal WeChat uses **“web QR binding + iLink long polling”**:
-> - Generate a QR code in the CyberStrikeAI web UI → scan and confirm with **WeChat on your phone**;
-> - On success, `robots.wechat` in `config.yaml` is updated automatically and iLink long polling starts (the app connects outbound to `ilinkai.weixin.qq.com`);
-> - **No** public callback URL on your server and **no** WeChat Open Platform app registration required.
-
-**Personal WeChat vs WeCom**
-
-| Item | Personal WeChat (iLink) | WeCom (Enterprise WeChat) |
-|------|-------------------------|---------------------------|
-| Use case | Private chat in personal WeChat | Custom app in WeCom |
-| Setup | QR scan in web UI | Admin console callback URL + Token |
-| Public IP needed? | No (outbound long poll only) | Yes (HTTPS callback reachable by WeCom) |
-| Config key | `robots.wechat` | `robots.wecom` |
-
-**Binding steps (in order)**
-
-1. **Log in to CyberStrikeAI web UI**
- **System settings** → **Robot settings** → click the **WeChat / iLink** card.
-
-2. **(Optional) Enable “Enable WeChat robot”**
- You can skip this on first bind; it is checked automatically after a successful bind.
-
-3. **Generate QR code**
- Click **“Generate QR code and bind”**. The QR code is valid for about **5 minutes**; regenerate if it expires.
-
-4. **Scan and confirm in WeChat**
- - Scan the QR code with WeChat on your phone;
- - Complete confirmation on the phone;
- - If WeChat shows a **pairing code**, enter it on the web page and click **Submit** (only some accounts need this).
-
-5. **Wait for binding to complete**
- When the page shows “Binding successful, WeChat robot enabled”, you’re done. `bot_token`, `ilink_bot_id`, etc. are saved to `config.yaml` and the iLink poll restarts automatically—**usually no manual service restart**.
-
-6. **Test in WeChat**
- Open the **private chat** with the CyberStrikeAI bot in WeChat and send “帮助” (help) or any text.
-
-**Field reference (WeChat)**
-
-| Field | Description |
-|-------|-------------|
-| Enable WeChat robot | Starts iLink long polling when checked; auto-enabled after bind |
-| Generate QR code and bind | Starts the scan-to-bind flow |
-| **Advanced** (defaults are fine) | |
-| API Base URL | Default `https://ilinkai.weixin.qq.com` |
-| Bot Type | Default `3` |
-| Bot Agent | Default `CyberStrikeAI/1.0` |
-| iLink Bot ID | Filled automatically after bind (read-only) |
-
-**How to use**
-
-- **Private chat only**—send text directly; **no @ needed**.
-- Group @-bot is **not** supported (unlike DingTalk/Lark groups).
-- **Text messages only**; images, voice, etc. are ignored or not supported.
-
-**Re-bind**
-
-- To bind a different WeChat account, click **“Re-bind”** on the robot settings page and scan again.
-- If you see “This WeChat account is already bound”, that account was bound before.
-
-**Common issues**
-
-| Symptom | What to do |
-|---------|------------|
-| QR code expired | Click “Generate QR code and bind” again (~5 min TTL) |
-| Phone asks for a pairing code | Enter the digits shown in WeChat on the web page |
-| Bound but no replies | Check logs for `微信 iLink 长轮询已启动` and `微信收到消息`; ensure “Enable WeChat robot” is on |
-| No reply after sleep / network drop | Auto-reconnect in ~5–60 s; restart CyberStrikeAI if still stuck |
-| Cannot generate QR code | Ensure outbound HTTPS to `https://ilinkai.weixin.qq.com` |
-
----
-
-### 3.5 Telegram
-
-> Telegram uses **Bot API long polling** (`getUpdates`): the app connects outbound to `api.telegram.org`—**no public callback URL needed**.
-
-1. Create a bot via **@BotFather** (`/newbot`) and copy the **Bot Token**.
-2. CyberStrikeAI → **System settings** → **Robot settings** → **Telegram**.
-3. Enable, paste the token, optionally allow group @ mentions → **Apply configuration**.
-
----
-
-### 3.6 Slack
-
-> Slack uses **Socket Mode** (outbound WebSocket): requires **Bot Token (xoxb-)** and **App-Level Token (xapp-)** with `connections:write`.
-
-1. Create an app at [api.slack.com](https://api.slack.com/apps) → enable **Socket Mode**.
-2. Create an App-Level Token; install the app to get a Bot Token.
-3. Subscribe to `message.im` and `app_mention` events.
-4. Paste both tokens in CyberStrikeAI → **Apply configuration**.
-
----
-
-### 3.7 Discord
-
-> Discord uses **Gateway WebSocket**—**no public callback URL needed**.
-
-1. [Discord Developer Portal](https://discord.com/developers/applications) → create app → **Bot** → copy **Token**.
-2. Enable **Message Content Intent** under Privileged Gateway Intents.
-3. Invite the bot with `Send Messages` permission.
-4. Paste token in CyberStrikeAI; optionally allow guild @ mentions → **Apply configuration**.
-
----
-
-### 3.8 QQ Bot
-
-> QQ Bot uses **QQ Open Platform WebSocket** (official `botgo` SDK) for C2C and group @—**no public callback URL needed**.
-
-1. Create a bot at [q.qq.com](https://q.qq.com) → get **App ID** and **Client Secret**.
-2. Add sandbox testers before going live.
-3. Subscribe to C2C and group @ events (WebSocket).
-4. Fill in CyberStrikeAI; use **Sandbox** for testing → **Apply configuration**.
-
----
-
-## 4. Bot commands
-
-Send these **text commands** to the bot on any connected platform (text only):
-
-| Command | Description |
-|---------|-------------|
-| **帮助** (help) | Show command help |
-| **列表** or **对话列表** (list) | List all conversation titles and IDs |
-| **切换 \** or **继续 \** | Continue in the given conversation |
-| **新对话** (new) | Start a new conversation |
-| **清空** (clear) | Clear current context (same effect as new conversation) |
-| **当前** (current) | Show current conversation ID and title |
-| **停止** (stop) | Abort the currently running task |
-| **角色** or **角色列表** (roles) | List all available roles (penetration testing, CTF, Web scan, etc.) |
-| **角色 \** or **切换角色 \** | Switch to the specified role |
-| **删除 \** | Delete the specified conversation |
-| **版本** (version) | Show current CyberStrikeAI version |
-
-Any other text is sent to the AI as a user message, same as in the web UI (e.g. penetration testing, security analysis).
-
----
-
-## 5. How to use (do I need to @ the bot?)
-
-- **Personal WeChat**: Send directly in the **private chat** with the bot; **no @ needed** (group chat not supported).
-- **DingTalk / Lark direct chat (recommended)**: **Search for the bot and open a direct chat**. Type “帮助” or any message; **no @ needed**.
-- **DingTalk / Lark group chat**: If the bot is in a group, only messages that **@ the bot** are received and answered; other group messages are ignored.
-
-Summary: **Personal WeChat and direct chat**—just send; **DingTalk/Lark in a group**—@ the bot first, then send.
-
----
-
-## 6. Recommended flow (so you don’t skip steps)
-
-**Personal WeChat (simplest—no open platform)**
-
-1. CyberStrikeAI web UI → System settings → Robot settings → **WeChat / iLink** → **Generate QR code and bind**.
-2. Scan with WeChat and confirm (enter pairing code on the web page if prompted).
-3. After binding, send “帮助” in the WeChat private chat to test.
-
-**DingTalk / Lark**
-
-1. **In the open platform**: Complete app creation, copy credentials, enable the bot (DingTalk: **Stream mode**), set permissions, and publish (Section 3).
-2. **In CyberStrikeAI**: System settings → Robot settings → Enable the platform, paste Client ID/App ID and Client Secret/App Secret → **Apply configuration**.
-3. **Restart the CyberStrikeAI process** (otherwise the long-lived connection is not established).
-4. **On your phone**: Open DingTalk or Lark, find the bot (direct chat or @ in a group), send “帮助” or any message to test.
-
-If the bot does not respond, see **Section 9 (troubleshooting)** and **Section 10 (common pitfalls)**.
-
----
-
-## 7. Config file example
-
-Example `robots` section in `config.yaml`:
-
-```yaml
-robots:
- wechat: # Personal WeChat iLink (auto-filled after QR bind; usually no manual edit)
- enabled: true
- bot_token: "your_bot_token@im.bot:..."
- ilink_bot_id: "your_bot_id@im.bot"
- ilink_user_id: "your_user_id@im.wechat"
- base_url: "https://ilinkai.weixin.qq.com"
- bot_type: "3"
- bot_agent: "CyberStrikeAI/1.0"
- dingtalk:
- enabled: true
- client_id: "your_dingtalk_app_key"
- client_secret: "your_dingtalk_app_secret"
- lark:
- enabled: true
- app_id: "your_lark_app_id"
- app_secret: "your_lark_app_secret"
- verify_token: ""
- wecom:
- enabled: false
- corp_id: ""
- agent_id: 0
- token: ""
- encoding_aes_key: ""
- secret: ""
- telegram:
- enabled: false
- bot_token: ""
- allow_group_messages: false
- slack:
- enabled: false
- bot_token: ""
- app_token: ""
- discord:
- enabled: false
- bot_token: ""
- allow_guild_messages: false
- qq:
- enabled: false
- app_id: ""
- client_secret: ""
- sandbox: true
-```
-
-After changing DingTalk/Lark/WeCom/Telegram/Slack/Discord/QQ settings, **Apply configuration** restarts the corresponding connections. Personal WeChat QR binding saves and restarts automatically.
-
----
-
-## 8. Testing without DingTalk/Lark installed
-
-You can verify bot logic with the **test API** (no DingTalk/Lark client needed):
-
-1. Log in to the CyberStrikeAI web UI (so you have a session).
-2. Call the test endpoint with curl (include your session Cookie):
-
-```bash
-# Replace YOUR_COOKIE with the Cookie from your browser (F12 → Network → any request → Request headers → Cookie)
-curl -X POST "http://localhost:8080/api/robot/test" \
- -H "Content-Type: application/json" \
- -H "Cookie: YOUR_COOKIE" \
- -d '{"platform":"dingtalk","user_id":"test_user","text":"帮助"}'
-```
-
-If the JSON response contains `"reply":"【CyberStrikeAI 机器人命令】..."`, command handling works. You can also try `"text":"列表"` or `"text":"当前"`.
-
-API: `POST /api/robot/test` (requires login). Body: `{"platform":"optional","user_id":"optional","text":"required"}`. Response: `{"reply":"..."}`.
-
----
-
-## 9. Troubleshooting: no response when sending messages
-
-### 9.1 Personal WeChat
-
-Check in this order:
-
-1. **Binding completed?**
- Robot settings should show “Connected” or a bound Bot ID; `robots.wechat.bot_token` in `config.yaml` must not be empty.
-
-2. **Enabled?**
- Confirm “Enable WeChat robot” is checked; restart CyberStrikeAI if you just changed settings.
-
-3. **Application logs**
- - On startup: `微信 iLink 长轮询已启动`;
- - After sending a message: `微信收到消息`; if missing, binding may have failed or `bot_token` is invalid—try **Re-bind**.
- - `微信 iLink 长轮询异常,将自动重连`: wait for auto-reconnect or restart.
-
-4. **Network**
- The server must reach `https://ilinkai.weixin.qq.com` (outbound HTTPS). If QR generation fails, check this first.
-
-5. **After sleep or network drop**
- Same as DingTalk/Lark: **auto-reconnect** in ~5–60 s; restart if still no response.
-
-### 9.2 DingTalk
-
-Check in this order:
-
-0. **After laptop sleep or network drop**
- DingTalk and Lark both use long-lived connections; they break when the machine sleeps or the network drops. The app **auto-reconnects** (retries within about 5–60 seconds). After wake or network recovery, wait a moment before sending; if there is still no response, restart the CyberStrikeAI process.
-
-1. **Client ID / Client Secret match the open platform exactly**
- Copy from “Credentials and basic info”; avoid typing. Watch **0** vs **o** and **1** vs **l** (e.g. `ding9gf9tiozuc504aer` has **504**, not 5o4).
-
-2. **Did you restart after saving?**
- The long-lived connection is created at **startup**. “Apply configuration” only updates the config file; you **must restart the CyberStrikeAI process** for the DingTalk connection to start.
-
-3. **Application logs**
- - On startup you should see: `钉钉 Stream 正在连接…`, `钉钉 Stream 已启动(无需公网),等待收消息`.
- - If you see `钉钉 Stream 长连接退出` with an error, it’s usually wrong **Client ID / Client Secret** or **Stream not enabled** in the open platform.
- - After sending a message in DingTalk, you should see `钉钉收到消息` in the logs; if not, the platform is not pushing to this app (check that the bot is enabled and **Stream mode** is selected).
-
-4. **Open platform**
- The app must be **published**. Under “Robot” you must enable **Stream** for receiving messages (HTTP callback only is not enough). Permission management must include robot receive/send message permissions.
-
----
-
-## 10. Common pitfalls
-
-- **Personal WeChat vs WeCom**: Personal WeChat uses `robots.wechat` + web QR bind; WeCom uses `robots.wecom` + admin callback URL—they are completely different.
-- **WeChat QR expired**: QR codes last ~5 minutes; regenerate instead of reusing an old one.
-- **Wrong bot type**: The “Custom” bot added in a DingTalk **group** (Webhook + sign secret) **cannot** be used for two-way chat. Only the **enterprise internal app** bot from the open platform is supported.
-- **Saved but not restarted**: After changing DingTalk/Lark robot settings you **must restart** the app (WeChat QR bind restarts the connection automatically).
-- **Client ID typo**: If the platform shows `504`, use `504` (not `5o4`); prefer copy/paste.
-- **DingTalk: only HTTP callback, no Stream**: This app receives messages via **Stream**. In the open platform, message reception must be **Stream mode**.
-- **App not published**: After changing the bot or permissions in the open platform, **publish a new version** under “Version management and release”, or changes won’t apply.
-
----
-
-## 11. Notes
-
-- All platforms: **text messages only**; other types (e.g. image, voice) are not supported and may be ignored.
-- Personal WeChat: **private chat only**—group @-bot is not supported.
-- Conversations are shared with the web UI: conversations created from the bot appear in the web “Conversations” list and vice versa.
-- Bot execution uses the same **Eino single/multi-agent** path as the web UI (`ProcessMessageForRobot`, with progress callbacks and process details stored in the DB); only the final reply is sent back to personal WeChat/DingTalk/Lark/WeCom in one message (no SSE). Default: `robot_default_agent_mode: eino_single`.
diff --git a/docs/workflow-graph.md b/docs/workflow-graph.md
deleted file mode 100644
index f2e6abdd..00000000
--- a/docs/workflow-graph.md
+++ /dev/null
@@ -1,553 +0,0 @@
-# CyberStrikeAI 图编排使用说明
-
-[English](workflow-graph_en.md)
-
-本文档说明 **图编排(Graph Orchestration)** 的完整使用方式:如何在画布上搭建流程、配置各类型节点、在节点之间传递数据,以及如何将流程绑定到角色并自动运行。
-
----
-
-## 一、在哪里使用图编排
-
-1. 登录 CyberStrikeAI Web 端
-2. 左侧导航进入 **图编排**
-3. 在左侧列表选择已有流程,或新建流程
-4. 在中央画布拖拽、连线、配置节点
-5. 填写流程 **ID**、**名称**、**描述** 后点击 **保存**
-
-保存后的流程可在 **角色管理** 中绑定到某个角色。绑定后,用户与该角色对话时会按流程图自动执行(`workflow_policy: auto`)。
-
----
-
-## 二、画布基本操作
-
-| 操作 | 说明 |
-|------|------|
-| 添加节点 | 点击画布上方节点类型按钮(开始、工具、Agent、条件、审批、输出、结束) |
-| 连线 | 点击 **连线**,依次点击源节点和目标节点;再次点击 **连线** 退出连线模式 |
-| 选中元素 | 单击节点或连线,右侧显示 **节点属性** |
-| 删除选中 | 点击 **删除选中** 删除当前节点或连线 |
-| 自动布局 | 点击 **自动布局** 整理节点位置 |
-| 试运行 | 点击 **试运行** 使用安全 dry-run 验证数据流;工具、Agent、审批不会真实执行 |
-| 删除流程 | 点击 **删除** 删除整个流程定义 |
-
-**硬性规则:** 每个流程至少包含 **1 个开始节点** 和 **1 个输出节点**;开始节点不能有入边,输出 / 结束节点不能有出边。保存时前端和后端都会执行严格校验。
-
----
-
-## 三、执行模型(先理解再配置)
-
-图编排按 **有向图** 执行,引擎从 **开始** 节点出发,沿连线依次运行下游节点。
-
-每次运行会维护一份内部状态,模板变量 `{{...}}` 从这里取值:
-
-| 内部状态 | 模板前缀 | 含义 |
-|----------|----------|------|
-| `inputs` | `{{inputs.xxx}}` | 流程启动时的输入(用户消息、会话 ID 等) |
-| `lastOutput` | `{{previous.xxx}}` | **上一个刚执行完** 的节点的输出 |
-| `outputs` | `{{outputs.xxx}}` | 全局 **命名变量池**(由节点的「输出变量名」写入) |
-| `nodeOutputs` | `{{节点ID.xxx}}` | 指定节点 ID 的完整输出对象 |
-| `metrics` | 运行详情中查看 | 节点耗时、工具调用数、可收集到的 token / cost 等指标 |
-
-### 3.1 `previous` 是什么?
-
-`{{previous.output}}` 表示 **紧邻的上一个执行节点** 的 `output` 字段。
-
-- 每执行完一个节点,引擎都会更新 `lastOutput`
-- **不是**「画布上画线的上游」,而是 **实际执行顺序上的上一步**
-
-示例:
-
-```text
-开始 → Agent A → Agent B
-```
-
-Agent B 的 `{{previous.output}}` = Agent A 的输出。
-
-但若中间有条件节点:
-
-```text
-开始 → Agent A → 条件 → Agent B
-```
-
-Agent B 的 `{{previous.output}}` = **条件节点** 的输出(`true` / `false`),**不是** Agent A 的结果。
-
-如果一个节点有 **多个上游节点** 同时连入,`previous` 会先按该节点的 **汇聚策略** 生成:
-
-| 汇聚策略 | 含义 | 适合场景 |
-|----------|------|----------|
-| `all_merge` | 合并所有上游输出,`previous.output` 为数组 | 默认推荐,综合多路结果 |
-| `last_by_canvas` | 按画布顺序取最后一个上游输出 | 明确只采用一路结果 |
-| `first_non_empty` | 取第一个非空输出 | 多路兜底 |
-| `fail_fast` | 任一上游失败则中止当前节点 | 关键链路、审批前置、安全检查 |
-
-### 3.2 `outputs` 是什么?
-
-`outputs` 是引擎在运行过程中维护的 **命名变量注册表**。
-
-当 Agent、工具、输出 等节点配置了 **输出变量名**(字段 `output_key`)后,节点执行成功会把结果写入:
-
-```text
-outputs["你填的变量名"] = 节点输出内容
-```
-
-之后 **任意下游节点** 都可以通过 `{{outputs.变量名}}` 引用,不要求两个节点直接相连。
-
-示例:
-
-- Agent A 的 **输出变量名** 填 `agent_result1`
-- Agent B 的 **输入来源** 填 `{{outputs.agent_result1}}`
-
-即使 A 和 B 之间隔着条件节点,B 仍能拿到 A 的输出。
-
-### 3.3 什么时候用 `previous`,什么时候用 `outputs`?
-
-| 场景 | 推荐写法 |
-|------|----------|
-| 两个节点 **直连**,只取上一步结果 | `{{previous.output}}` |
-| 中间有其他节点(条件、工具、审批等) | `{{outputs.变量名}}` |
-| 需要引用 **更早** 的某个节点结果 | `{{outputs.变量名}}` 或 `{{节点ID.output}}` |
-| 条件判断要基于某 Agent 的输出 | `{{outputs.变量名}} != ""` |
-| 读取用户最初输入 | `{{inputs.message}}` |
-
-**记忆口诀:**
-
-- `previous` = 上一步(链式、紧邻)
-- `outputs` = 按名字取(跨节点、可回溯)
-
----
-
-## 四、模板语法
-
-### 4.1 基本格式
-
-```text
-{{变量路径}}
-```
-
-支持字母、数字、下划线、点、连字符,例如:
-
-```text
-{{previous.output}}
-{{outputs.agent_result1}}
-{{inputs.message}}
-{{inputs.conversationId}}
-{{previous.matched}}
-{{node-abc123.output}}
-```
-
-### 4.2 可用路径一览
-
-| 路径 | 说明 |
-|------|------|
-| `{{inputs.message}}` | 用户消息(开始节点输入) |
-| `{{inputs.conversationId}}` | 会话 ID |
-| `{{inputs.projectId}}` | 项目 ID |
-| `{{previous.output}}` | 上一节点主输出 |
-| `{{previous.matched}}` | 上一条件节点的匹配结果(`true` / `false`) |
-| `{{outputs.变量名}}` | 某节点注册过的命名输出 |
-| `{{节点ID.output}}` | 指定节点 ID 的 `output` 字段 |
-| `{{previous.kind}}` | 上一节点输出类型,如 `agent` / `tool` / `condition` |
-| `{{previous.status}}` | 上一节点状态,如 `completed` / `failed` / `simulated` |
-
-节点输出会保留兼容字段(如 `output`、`matched`),同时带有结构化字段:
-
-```json
-{
- "kind": "agent",
- "node_id": "node-2",
- "node_type": "agent",
- "status": "completed",
- "output": "..."
-}
-```
-
-### 4.3 条件表达式
-
-条件节点和连线条件支持比较、文本匹配、正则、逻辑组合与安全 JSONPath/JQ 路径读取:
-
-```text
-{{outputs.agent_result1}} != ""
-{{previous.output}} == "ok"
-{{outputs.count}} >= 100
-{{previous.output}} contains "success"
-{{previous.output}} matches "^ok"
-{{outputs.risk_score}} >= 8 && {{previous.output}} != ""
-jsonpath({{previous.output}}, "$.status") == "ok"
-jq({{outputs.scan}}, ".severity") == "high"
-```
-
-规则:
-
-- 支持 `==`、`!=`、`>`、`>=`、`<`、`<=`
-- 支持 `contains` 子串匹配与 `matches` 正则匹配
-- 支持简单 `&&` / `||`
-- 支持 `jsonpath(value, "$.path")` 与 `jq(value, ".path")` 的**安全路径子集**,仅做字段读取,不执行任意脚本
-- 比较两侧会自动去掉首尾空格和引号
-- 无比较符时,非空且不为 `false` / `0` / `null` 视为真
-- 保存时会静态校验表达式格式、JSONPath/JQ 路径和正则语法
-
-### 4.4 嵌套字段绑定
-
-节点的字段绑定除 `output`、`message` 等普通字段外,也支持 JSONPath/JQ 风格路径:
-
-| 绑定配置 | 含义 |
-|----------|------|
-| `from=previous, field=$.status` | 从上一节点输出对象读取 `status` |
-| `from=outputs, field=$.scan.severity` | 从命名输出中读取嵌套字段 |
-| `from=node-1, field=.output.items[0]` | 从指定节点输出读取数组元素 |
-
----
-
-## 五、节点类型与配置
-
-### 5.1 开始(start)
-
-流程入口,将用户输入注入 `inputs`。
-
-| 字段 | 说明 | 默认值 |
-|------|------|--------|
-| 输入变量 | 逗号分隔的输入键名 | `message, conversationId, projectId` |
-
-开始节点输出包含:`output`、`message`、`conversationId`、`projectId`。
-
-### 5.2 Agent(agent)
-
-调用大模型 Agent 处理任务,支持多种运行模式。
-
-| 字段 | 说明 | 默认值 |
-|------|------|--------|
-| Agent 模式 | `eino_single` / `deep` / `plan_execute` / `supervisor` | `eino_single` |
-| 输入来源 | 上游数据的模板表达式 | `{{previous.output}}` |
-| 节点指令 | 本节点要完成的任务描述 | 空 |
-| 输出变量名 | 写入 `outputs` 的键名 | `agent_result` |
-| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` |
-
-**消息拼装规则:**
-
-- 仅填 **节点指令**:直接把指令发给 Agent
-- 仅填 **输入来源**:生成「请基于上游节点输出继续处理:…」
-- 两者都填:合并为「上游输入 + 节点指令」
-
-Agent 节点执行后:
-
-- `previous.output` 更新为本节点响应文本
-- 若配置了 **输出变量名**,同时写入 `outputs[输出变量名]`
-- Agent 子图在 Eino 中拆为 `prepare → execute → finalize`,便于 trace 与后续局部 checkpoint
-
-### 5.3 工具(tool)
-
-调用已启用的 MCP 工具。
-
-| 字段 | 说明 | 默认值 |
-|------|------|--------|
-| MCP 工具 | 工具名称(必填) | — |
-| 参数模板 | JSON,支持 `{{...}}` 模板 | `{}` |
-| 超时秒数 | 可选 | 空 |
-| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` |
-
-示例参数模板:
-
-```json
-{"target": "{{inputs.message}}", "port": "443"}
-```
-
-若配置了 **输出变量名**,工具返回结果会写入 `outputs`。
-
-### 5.4 条件(condition)
-
-根据表达式计算分支,输出 `matched`(`true` / `false`)。
-
-| 字段 | 说明 | 默认值 |
-|------|------|--------|
-| 条件表达式 | 支持 `{{...}}` 与 `==` / `!=` | `{{previous.output}} != ""` |
-| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` |
-
-**分支规则:**
-
-- 从条件节点连出的 **第一条线** 默认为 **「是」** 分支(`matched == true`)
-- **第二条线** 默认为 **「否」** 分支(`matched == false`)
-- 连线标签可写 `是` / `否`(或 `yes` / `no`、`true` / `false`)辅助识别
-- 第三条及以后的出边需在 **连线条件** 中自定义表达式
-
-连线条件示例(选中连线后在右侧配置):
-
-```text
-{{previous.matched}} == "true"
-{{previous.matched}} == "false"
-```
-
-### 5.5 审批(hitl)
-
-人工确认检查点。流程运行到该节点前会通过 Eino interrupt/checkpoint 暂停,等待 API 或监控面板审批后恢复。
-
-| 字段 | 说明 | 默认值 |
-|------|------|--------|
-| 审批提示 | 支持模板 | `请审批该步骤是否继续执行` |
-| 提示字段绑定 | 留空审批提示时,从绑定字段读取说明 | `previous.output` |
-| 审批方 | `human` / `audit_agent` | `human` |
-| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` |
-
-HITL 等待信息会记录:
-
-- `checkpointId`
-- interrupt `beforeNodes`
-- resume target / address / path
-- resume payload schema(`approved`、`comment`)
-
-### 5.6 输出(output)
-
-将流程最终结果写入 `outputs`,供结束摘要和对话展示使用。
-
-| 字段 | 说明 | 默认值 |
-|------|------|--------|
-| 输出变量名 | 必填,最终结果的键名 | `result` |
-| 变量来源 | 模板表达式,决定写入的值 | `{{previous.output}}` |
-| 固定输出值 | 可选,填写后覆盖变量来源 | 空 |
-| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` |
-
-**注意:** 输出节点是流程的「出口」,不应再有出边。
-
-### 5.7 结束(end)
-
-可选节点,用于生成结束摘要模板(角色绑定流程中较少单独使用)。
-
-| 字段 | 说明 | 默认值 |
-|------|------|--------|
-| 结束摘要模板 | 支持 `{{outputs.xxx}}` | `{{outputs.result}}` |
-| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` |
-
----
-
-## 六、连线配置
-
-选中 **连线** 后,右侧可配置 **连线条件**。
-
-| 场景 | 示例 |
-|------|------|
-| 普通节点后的过滤 | `{{previous.output}} == "ok"` |
-| 条件节点「是」分支 | `{{previous.matched}} == "true"` |
-| 条件节点「否」分支 | `{{previous.matched}} == "false"` |
-
-若不填连线条件:
-
-- 非条件节点:连线始终放行
-- 条件节点:按出边顺序自动分配是/否分支
-
----
-
-## 七、完整示例:跨条件节点传递 Agent 输出
-
-### 7.1 流程结构
-
-```text
-开始 → Agent(生成初始值)→ 条件 → Agent(加工)→ 输出
- ↘ 否 → 输出
-```
-
-### 7.2 节点配置
-
-**Agent 1(第一个 Agent)**
-
-| 字段 | 值 |
-|------|-----|
-| 节点指令 | 只输出 `123333333` |
-| 输出变量名 | `agent_result1` |
-
-**条件**
-
-| 字段 | 值 |
-|------|-----|
-| 条件表达式 | `{{outputs.agent_result1}} != ""` |
-
-**Agent 2(第二个 Agent)**
-
-| 字段 | 值 |
-|------|-----|
-| 输入来源 | `{{outputs.agent_result1}}` |
-| 节点指令 | 在输入基础上加 100,然后输出 |
-| 输出变量名 | `agent_result` |
-
-**输出**
-
-| 字段 | 值 |
-|------|-----|
-| 输出变量名 | `result` |
-| 变量来源 | `{{outputs.agent_result}}` |
-
-### 7.3 常见错误
-
-| 错误配置 | 原因 |
-|----------|------|
-| Agent 2 输入来源写 `{{previous.output}}` | `previous` 指向条件节点,得到的是 `true`/`false`,不是 Agent 1 的文本 |
-| 未给 Agent 1 填输出变量名 | `outputs.agent_result1` 不存在,下游取到空值 |
-| 条件表达式写 `{{previous.output}}` | 判断的是开始节点或上一节点的输出,而非 Agent 1 的命名变量 |
-
----
-
-## 八、绑定角色并运行
-
-### 8.1 在角色管理中绑定
-
-1. 进入 **角色管理**,编辑或新建角色
-2. 选择 **工作流 / 图编排** 绑定的流程 ID
-3. 策略设为 `auto`(默认:有 `workflow_id` 时自动执行)
-4. 保存角色
-
-也可在角色 YAML 中直接配置:
-
-```yaml
-name: 工作流测试
-workflow_id: "1233"
-workflow_version: latest
-workflow_policy: auto
-```
-
-### 8.2 运行效果
-
-用户选择该角色并发送消息后:
-
-1. 引擎加载对应 `graph_json` 并按图执行
-2. 对话页可看到 `workflow_start`、`workflow_node_start`、Agent 推理等进度事件
-3. 流程结束后返回摘要,列出 `outputs` 中所有命名输出
-
-若未配置输出节点或条件未命中,`outputs` 可能为空,摘要会提示检查输出节点与分支。
-
----
-
-## 九、调试、试运行与复盘
-
-### 9.1 安全试运行(dry-run)
-
-画布工具栏点击 **试运行**,输入一条测试消息即可模拟执行流程。
-
-dry-run 的安全边界:
-
-- `start` / `condition` / `output` / `end` 会按真实逻辑计算
-- `tool` 不会真实调用 MCP,只返回 `[dry-run] tool call skipped`
-- `agent` 不会真实调用模型,只返回 `[dry-run] agent execution skipped`
-- `hitl` 不会暂停,只模拟通过
-
-相关 API:
-
-```http
-POST /api/workflows/dry-run
-```
-
-请求体:
-
-```json
-{
- "graph": { "nodes": [], "edges": [], "config": {} },
- "inputs": { "message": "ping" }
-}
-```
-
-响应包含:
-
-- `outputs`
-- `nodeOutputs`
-- `trace`
-- `metrics`
-- `replayScript`
-
-### 9.2 运行详情与 replay
-
-运行后可查询完整节点执行轨迹:
-
-```http
-GET /api/workflows/runs/{runId}
-```
-
-返回 `run` 与 `nodeRuns`,每个节点记录包含:
-
-- input 快照
-- output 快照
-- status / error
-- started_at / finished_at
-- `duration_ms`
-
-复盘接口:
-
-```http
-GET /api/workflows/runs/{runId}/replay
-```
-
-该接口只根据已保存的 `nodeRuns` 生成步骤,不会重新执行工具或 Agent。
-
-### 9.3 指标(metrics)
-
-工作流会尽量累计:
-
-- `node_count`
-- `duration_ms`
-- `tool_call_count`
-- Agent progress 中可收集到的 `prompt_tokens` / `completion_tokens` / `total_tokens` / `cost`
-
-token 与成本是否存在取决于底层模型/Agent 事件是否上报 usage。
-
----
-
-## 十、保存前校验规则
-
-保存时系统会自动检查:
-
-| 规则 | 说明 |
-|------|------|
-| 必须有开始节点 | 至少 1 个 `start` |
-| 必须有输出节点 | 至少 1 个 `output`,且填写输出变量名 |
-| 连线合法 | 源/目标节点存在,不能自环 |
-| 开始节点无入边 | 开始节点不能被指向 |
-| 输出 / 结束节点无出边 | 输出 / 结束节点后不应再连线 |
-| 非开始节点必须有入边 | 避免孤岛节点 |
-| 非输出 / 结束节点必须有出边 | 避免执行到死路 |
-| 无环路 | Workflow 编排必须是 DAG |
-| 可达性 | 所有节点必须能从开始节点到达,并能最终到达 output/end |
-| 工具节点 | 必须选择 MCP 工具;参数 JSON 必须合法;超时必须为正整数 |
-| Agent 节点 | 必须填写节点指令或输入绑定;必须填写输出变量名 |
-| 条件节点 | 必须填写表达式;需要 1~2 条出边;分支必须标记是/否且不能重复 |
-| 连线条件 | 表达式、正则、JSONPath/JQ 路径必须通过静态校验 |
-| 汇聚策略 | 必须是 `all_merge` / `last_by_canvas` / `first_non_empty` / `fail_fast` |
-
----
-
-## 十一、排错指南
-
-| 现象 | 可能原因 | 处理建议 |
-|------|----------|----------|
-| 下游拿到空值 | 上游未配置输出变量名 | 给上游 Agent/工具填 **输出变量名**,下游用 `{{outputs.xxx}}` |
-| 下游拿到 `true`/`false` | 误用 `{{previous.output}}`,上一步是条件节点 | 改用 `{{outputs.xxx}}` |
-| 条件总走「否」 | 表达式与真实输出格式不一致 | 检查 Agent 输出是否带引号、换行;用 `!= ""` 先验证 |
-| 流程无最终输出 | 未命中输出节点所在分支 | 检查条件分支连线;确保至少一条路径到达 **输出** 节点 |
-| 角色对话未跑流程 | 角色未绑定或未启用 | 确认 `workflow_id`、`workflow_policy: auto`、流程 `enabled: true` |
-| 工具节点失败 | 参数 JSON 不合法或工具未启用 | 检查参数模板;在 MCP 中启用对应工具 |
-| 保存失败提示分支非法 | 条件节点出边未标记是/否或重复 | 选中连线,设置条件分支为 `true` 或 `false` |
-| 多上游结果不符合预期 | 汇聚策略不合适 | 根据场景改为 `all_merge` / `first_non_empty` / `last_by_canvas` / `fail_fast` |
-| 嵌套字段取不到 | JSONPath/JQ 路径不符合安全子集 | 使用 `$.a.b[0]` 或 `.a.b[0]`,不要用通配符/递归/表达式 |
-
----
-
-## 十二、最佳实践
-
-1. **命名规范**:为每个需要被引用的节点设置有意义的输出变量名,如 `scan_result`、`parsed_targets`,避免都叫 `agent_result`。
-2. **跨节点传参优先用 `outputs`**:只要中间可能插入条件、工具、审批节点,就应用命名变量。
-3. **`previous` 仅用于直连**:A → B 且无中间节点时,`{{previous.output}}` 最简洁。
-4. **条件判断引用源数据**:判断 Agent 输出时用 `{{outputs.xxx}}`,不要用 `{{previous.output}}`(除非条件紧跟在目标 Agent 之后)。
-5. **每条路径都要有出口**:确保「是」「否」分支最终都能到达 **输出** 节点(或你期望的终点)。
-6. **多上游节点显式选择汇聚策略**:综合结果用 `all_merge`,兜底用 `first_non_empty`,关键链路用 `fail_fast`。
-7. **嵌套 JSON 用 JSONPath/JQ 安全路径**:例如 `jsonpath({{previous.output}}, "$.status") == "ok"`。
-8. **保存前先 dry-run**:用简单消息验证数据传递和分支,再绑定角色真实执行。
-
----
-
-## 十三、相关代码位置(开发者参考)
-
-| 模块 | 路径 |
-|------|------|
-| 执行引擎 | `internal/workflow/runner.go` |
-| Eino 编译 / checkpoint / HITL | `internal/workflow/eino_compile.go` |
-| 图校验 | `internal/workflow/validation.go` |
-| 表达式 / JSONPath / 汇聚 | `internal/workflow/expression.go`、`jsonpath.go`、`join.go` |
-| dry-run / replay 数据 | `internal/workflow/dry_run.go`、`internal/handler/workflow_run.go` |
-| 画布前端 | `web/static/js/workflows.js` |
-| 流程 API | `internal/handler/workflow.go` |
-| 角色绑定 | `internal/config/config.go`(`workflow_id` 字段) |
diff --git a/docs/workflow-graph_en.md b/docs/workflow-graph_en.md
deleted file mode 100644
index a4b4223c..00000000
--- a/docs/workflow-graph_en.md
+++ /dev/null
@@ -1,553 +0,0 @@
-# CyberStrikeAI Graph Orchestration Guide
-
-[中文](workflow-graph.md)
-
-This document explains how to use **Graph Orchestration**: building workflows on the canvas, configuring node types, passing data between nodes, and binding a graph to a role for automatic execution.
-
----
-
-## 1. Where to find Graph Orchestration
-
-1. Log in to the CyberStrikeAI web UI.
-2. Open **Graph Orchestration** in the left sidebar.
-3. Select an existing workflow from the list, or create a new one.
-4. Drag nodes, draw edges, and configure properties on the canvas.
-5. Fill in **ID**, **Name**, and **Description**, then click **Save**.
-
-Saved workflows can be bound to a role under **Role Management**. When `workflow_policy` is `auto`, chatting with that role runs the bound graph automatically.
-
----
-
-## 2. Canvas basics
-
-| Action | Description |
-|--------|-------------|
-| Add node | Click a node type button above the canvas (Start, Tool, Agent, Condition, HITL, Output, End) |
-| Connect | Click **Connect**, then click source and target nodes; click **Connect** again to exit connect mode |
-| Select | Click a node or edge; properties appear in the right panel |
-| Delete selected | Remove the current node or edge |
-| Auto layout | Rearrange node positions |
-| Dry run | Safely simulate data flow; Tool, Agent, and HITL nodes are not executed for real |
-| Delete workflow | Remove the entire workflow definition |
-
-**Hard requirements:** Every workflow needs at least **one Start node** and **one Output node**. Start nodes must not have incoming edges; Output / End nodes must not have outgoing edges. Both frontend and backend run strict validation before save.
-
----
-
-## 3. Execution model (read this before configuring)
-
-The engine executes the workflow as a **directed graph**, starting from the **Start** node and following edges to downstream nodes.
-
-During a run, the engine keeps internal state. Template expressions `{{...}}` read from that state:
-
-| Internal state | Template prefix | Meaning |
-|----------------|-----------------|---------|
-| `inputs` | `{{inputs.xxx}}` | Workflow inputs at start (user message, conversation ID, etc.) |
-| `lastOutput` | `{{previous.xxx}}` | Output of the **most recently executed** node |
-| `outputs` | `{{outputs.xxx}}` | Global **named variable pool** (written by nodes with an output key) |
-| `nodeOutputs` | `{{nodeId.xxx}}` | Full output object of a specific node ID |
-| `metrics` | available in run details | Node duration, tool call count, and usage/cost metrics when reported |
-
-### 3.1 What is `previous`?
-
-`{{previous.output}}` is the `output` field of the **immediately preceding executed node**.
-
-- After every node finishes, the engine updates `lastOutput`.
-- It is **not** “the node drawn upstream on the canvas”; it is **the previous step in actual execution order**.
-
-Example:
-
-```text
-Start → Agent A → Agent B
-```
-
-For Agent B, `{{previous.output}}` = Agent A’s output.
-
-With a condition in between:
-
-```text
-Start → Agent A → Condition → Agent B
-```
-
-For Agent B, `{{previous.output}}` = the **condition node** output (`true` / `false`), **not** Agent A’s result.
-
-If a node has **multiple upstream nodes**, `previous` is built by that node’s **join strategy** first:
-
-| Join strategy | Meaning | Use case |
-|---------------|---------|----------|
-| `all_merge` | Merge all upstream outputs; `previous.output` is an array | Default; aggregate multiple results |
-| `last_by_canvas` | Use the last upstream output by canvas order | Explicitly use one branch |
-| `first_non_empty` | Use the first non-empty output | Fallback chains |
-| `fail_fast` | Stop the node if any upstream failed | Critical gates, approval prechecks, safety checks |
-
-### 3.2 What is `outputs`?
-
-`outputs` is a **named variable registry** maintained by the engine during execution.
-
-When an Agent, Tool, or Output node sets an **Output variable name** (`output_key`), the result is stored as:
-
-```text
-outputs["your_variable_name"] = node_output
-```
-
-Any downstream node can then reference it via `{{outputs.variable_name}}`, even if other nodes sit in between.
-
-Example:
-
-- Agent A **Output variable name**: `agent_result1`
-- Agent B **Input source**: `{{outputs.agent_result1}}`
-
-Agent B still receives Agent A’s output even when a condition node lies between them.
-
-### 3.3 When to use `previous` vs `outputs`
-
-| Scenario | Recommended |
-|----------|-------------|
-| Two nodes are **directly connected**; you only need the last step | `{{previous.output}}` |
-| Other nodes sit in between (condition, tool, HITL, etc.) | `{{outputs.variable_name}}` |
-| Reference output from an **earlier** node | `{{outputs.variable_name}}` or `{{nodeId.output}}` |
-| Condition should test an Agent’s output | `{{outputs.variable_name}} != ""` |
-| Read the original user input | `{{inputs.message}}` |
-
-**Rule of thumb:**
-
-- `previous` = last step (chained, adjacent)
-- `outputs` = by name (cross-node, look back)
-
----
-
-## 4. Template syntax
-
-### 4.1 Basic format
-
-```text
-{{path.to.value}}
-```
-
-Allowed characters in paths: letters, digits, underscore, dot, hyphen. Examples:
-
-```text
-{{previous.output}}
-{{outputs.agent_result1}}
-{{inputs.message}}
-{{inputs.conversationId}}
-{{previous.matched}}
-{{node-abc123.output}}
-```
-
-### 4.2 Available paths
-
-| Path | Description |
-|------|-------------|
-| `{{inputs.message}}` | User message (Start node input) |
-| `{{inputs.conversationId}}` | Conversation ID |
-| `{{inputs.projectId}}` | Project ID |
-| `{{previous.output}}` | Primary output of the previous node |
-| `{{previous.matched}}` | Match result of the previous condition node (`true` / `false`) |
-| `{{outputs.variable_name}}` | Named output registered by a node |
-| `{{nodeId.output}}` | `output` field of the node with that ID |
-| `{{previous.kind}}` | Previous node output kind, e.g. `agent` / `tool` / `condition` |
-| `{{previous.status}}` | Previous node status, e.g. `completed` / `failed` / `simulated` |
-
-Node outputs keep compatibility fields such as `output` and `matched`, and also include a structured envelope:
-
-```json
-{
- "kind": "agent",
- "node_id": "node-2",
- "node_type": "agent",
- "status": "completed",
- "output": "..."
-}
-```
-
-### 4.3 Condition expressions
-
-Condition nodes and edge conditions support comparisons, text matching, regex, logical operators, and safe JSONPath/JQ path reads:
-
-```text
-{{outputs.agent_result1}} != ""
-{{previous.output}} == "ok"
-{{outputs.count}} >= 100
-{{previous.output}} contains "success"
-{{previous.output}} matches "^ok"
-{{outputs.risk_score}} >= 8 && {{previous.output}} != ""
-jsonpath({{previous.output}}, "$.status") == "ok"
-jq({{outputs.scan}}, ".severity") == "high"
-```
-
-Rules:
-
-- Operators: `==`, `!=`, `>`, `>=`, `<`, `<=`
-- `contains` checks substrings; `matches` checks regular expressions
-- Simple `&&` / `||` is supported
-- `jsonpath(value, "$.path")` and `jq(value, ".path")` support a **safe path-only subset**; no arbitrary script execution
-- Leading/trailing spaces and quotes are trimmed before comparison
-- Without a comparator, non-empty values that are not `false`, `0`, or `null` are treated as true
-- Expressions, regexes, and JSONPath/JQ paths are statically validated before save
-
-### 4.4 Nested field binding
-
-Field bindings can read ordinary fields such as `output` or `message`, and also JSONPath/JQ-style paths:
-
-| Binding | Meaning |
-|---------|---------|
-| `from=previous, field=$.status` | Read `status` from previous output |
-| `from=outputs, field=$.scan.severity` | Read a nested field from named outputs |
-| `from=node-1, field=.output.items[0]` | Read an array element from a specific node output |
-
----
-
-## 5. Node types and configuration
-
-### 5.1 Start
-
-Workflow entry point; injects user input into `inputs`.
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| Input keys | Comma-separated input key names | `message, conversationId, projectId` |
-
-Start node output includes: `output`, `message`, `conversationId`, `projectId`.
-
-### 5.2 Agent
-
-Runs an LLM Agent task. Supports multiple modes.
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| Agent mode | `eino_single` / `deep` / `plan_execute` / `supervisor` | `eino_single` |
-| Input source | Template for upstream data | `{{previous.output}}` |
-| Node instruction | Task description for this node | empty |
-| Output variable name | Key written into `outputs` | `agent_result` |
-| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` |
-
-**Message assembly:**
-
-- Instruction only → send instruction to the Agent
-- Input source only → “Continue based on upstream output: …”
-- Both → combined “upstream input + node instruction”
-
-After execution:
-
-- `previous.output` becomes this node’s response text
-- If **Output variable name** is set, the value is also stored in `outputs[variable_name]`
-- In the Eino graph, the Agent node is split into `prepare → execute → finalize` for clearer trace and future checkpointing
-
-### 5.3 Tool
-
-Calls an enabled MCP tool.
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| MCP tool | Tool name (required) | — |
-| Argument template | JSON with `{{...}}` templates | `{}` |
-| Timeout (seconds) | Optional | empty |
-| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` |
-
-Example argument template:
-
-```json
-{"target": "{{inputs.message}}", "port": "443"}
-```
-
-If an output variable name is configured, the tool result is written to `outputs`.
-
-### 5.4 Condition
-
-Evaluates an expression and outputs `matched` (`true` / `false`).
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| Expression | Supports `{{...}}` and `==` / `!=` | `{{previous.output}} != ""` |
-| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` |
-
-**Branching rules:**
-
-- The **first outgoing edge** defaults to the **“yes”** branch (`matched == true`)
-- The **second outgoing edge** defaults to the **“no”** branch (`matched == false`)
-- Edge labels such as `是` / `否` (or `yes` / `no`, `true` / `false`) help identify branches
-- A third or later edge needs a custom **edge condition**
-
-Edge condition examples (select an edge, configure in the right panel):
-
-```text
-{{previous.matched}} == "true"
-{{previous.matched}} == "false"
-```
-
-### 5.5 HITL (human-in-the-loop)
-
-Human approval checkpoint. The run pauses before this node through Eino interrupt/checkpoint and resumes after approval via API or the monitor panel.
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| Prompt | Supports templates | `Please approve before continuing` |
-| Prompt binding | If prompt text is empty, read approval text from a bound field | `previous.output` |
-| Reviewer | `human` / `audit_agent` | `human` |
-| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` |
-
-Pending HITL metadata records:
-
-- `checkpointId`
-- interrupt `beforeNodes`
-- resume target / address / path
-- resume payload schema (`approved`, `comment`)
-
-### 5.6 Output
-
-Writes the final workflow result into `outputs` for summary and chat display.
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| Output variable name | Required key for the final result | `result` |
-| Variable source | Template deciding what to write | `{{previous.output}}` |
-| Static output value | Optional; overrides variable source when set | empty |
-| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` |
-
-**Note:** Output nodes are workflow exits and must not have outgoing edges.
-
-### 5.7 End
-
-Optional node for an end summary template (less common in role-bound flows).
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| Result template | Supports `{{outputs.xxx}}` | `{{outputs.result}}` |
-| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` |
-
----
-
-## 6. Edge configuration
-
-Select an **edge** to configure its **condition** in the right panel.
-
-| Scenario | Example |
-|----------|---------|
-| Filter after a normal node | `{{previous.output}} == "ok"` |
-| “Yes” branch from a condition | `{{previous.matched}} == "true"` |
-| “No” branch from a condition | `{{previous.matched}} == "false"` |
-
-If no edge condition is set:
-
-- Non-condition nodes: edge is always allowed
-- Condition nodes: yes/no branches are assigned by edge order automatically
-
----
-
-## 7. Full example: passing Agent output across a condition
-
-### 7.1 Graph structure
-
-```text
-Start → Agent (initial value) → Condition → Agent (transform) → Output
- ↘ no → Output
-```
-
-### 7.2 Node configuration
-
-**Agent 1**
-
-| Field | Value |
-|-------|-------|
-| Node instruction | Output only `123333333` |
-| Output variable name | `agent_result1` |
-
-**Condition**
-
-| Field | Value |
-|-------|-------|
-| Expression | `{{outputs.agent_result1}} != ""` |
-
-**Agent 2**
-
-| Field | Value |
-|-------|-------|
-| Input source | `{{outputs.agent_result1}}` |
-| Node instruction | Add 100 to the input, then output |
-| Output variable name | `agent_result` |
-
-**Output**
-
-| Field | Value |
-|-------|-------|
-| Output variable name | `result` |
-| Variable source | `{{outputs.agent_result}}` |
-
-### 7.3 Common mistakes
-
-| Wrong config | Why it fails |
-|--------------|--------------|
-| Agent 2 input source = `{{previous.output}}` | `previous` points to the condition node → `true`/`false`, not Agent 1’s text |
-| Agent 1 has no output variable name | `outputs.agent_result1` does not exist → empty downstream |
-| Condition uses `{{previous.output}}` | Tests the wrong upstream value instead of Agent 1’s named output |
-
----
-
-## 8. Bind to a role and run
-
-### 8.1 Bind in Role Management
-
-1. Open **Role Management**, edit or create a role.
-2. Select the workflow / graph ID to bind.
-3. Set policy to `auto` (default when `workflow_id` is set).
-4. Save the role.
-
-You can also configure this in role YAML:
-
-```yaml
-name: workflow-test
-workflow_id: "1233"
-workflow_version: latest
-workflow_policy: auto
-```
-
-### 8.2 Runtime behavior
-
-When a user chats with that role:
-
-1. The engine loads `graph_json` and executes the graph.
-2. The chat UI shows progress events (`workflow_start`, `workflow_node_start`, Agent reasoning, etc.).
-3. When finished, a summary lists all named entries in `outputs`.
-
-If no Output node is reached or no branch matches, `outputs` may be empty and the summary will suggest checking the Output node and branches.
-
----
-
-## 9. Debugging, dry-run, and replay
-
-### 9.1 Safe dry-run
-
-Click **Dry run** on the canvas toolbar and enter a test message to simulate the workflow.
-
-Dry-run safety rules:
-
-- `start` / `condition` / `output` / `end` use real logic
-- `tool` does not call MCP; it returns `[dry-run] tool call skipped`
-- `agent` does not call the model; it returns `[dry-run] agent execution skipped`
-- `hitl` does not pause; it simulates approval
-
-API:
-
-```http
-POST /api/workflows/dry-run
-```
-
-Request:
-
-```json
-{
- "graph": { "nodes": [], "edges": [], "config": {} },
- "inputs": { "message": "ping" }
-}
-```
-
-Response includes:
-
-- `outputs`
-- `nodeOutputs`
-- `trace`
-- `metrics`
-- `replayScript`
-
-### 9.2 Run details and replay
-
-Query full node execution traces after a run:
-
-```http
-GET /api/workflows/runs/{runId}
-```
-
-The response contains `run` and `nodeRuns`. Each node run records:
-
-- input snapshot
-- output snapshot
-- status / error
-- started_at / finished_at
-- `duration_ms`
-
-Replay API:
-
-```http
-GET /api/workflows/runs/{runId}/replay
-```
-
-This generates replay steps from saved `nodeRuns`; it does not re-execute tools or Agents.
-
-### 9.3 Metrics
-
-The workflow accumulates, when available:
-
-- `node_count`
-- `duration_ms`
-- `tool_call_count`
-- Agent progress usage such as `prompt_tokens` / `completion_tokens` / `total_tokens` / `cost`
-
-Token and cost metrics depend on whether the underlying model/Agent events report usage.
-
----
-
-## 10. Validation before save
-
-On save, the system checks:
-
-| Rule | Description |
-|------|-------------|
-| Start node required | At least one `start` node |
-| Output node required | At least one `output` node with an output variable name |
-| Valid edges | Source and target exist; no self-loops |
-| Start has no incoming edges | Start must not be targeted |
-| Output / End has no outgoing edges | Nothing after Output / End |
-| Non-start nodes must have incoming edges | Prevent orphan nodes |
-| Non-output/end nodes must have outgoing edges | Prevent dead ends |
-| No cycles | Workflow orchestration must be a DAG |
-| Reachability | Every node must be reachable from Start and eventually reach output/end |
-| Tool nodes | MCP tool required; argument JSON must be valid; timeout must be a positive integer |
-| Agent nodes | Must have node instruction or input binding; output variable name required |
-| Condition nodes | Expression required; 1–2 outgoing edges; branches must be yes/no and unique |
-| Edge conditions | Expressions, regexes, and JSONPath/JQ paths must pass static validation |
-| Join strategy | Must be `all_merge` / `last_by_canvas` / `first_non_empty` / `fail_fast` |
-
----
-
-## 11. Troubleshooting
-
-| Symptom | Likely cause | Fix |
-|---------|--------------|-----|
-| Downstream gets empty value | Upstream has no output variable name | Set **Output variable name** on upstream; use `{{outputs.xxx}}` downstream |
-| Downstream gets `true`/`false` | Used `{{previous.output}}` while previous node is a condition | Use `{{outputs.xxx}}` instead |
-| Condition always takes “no” | Expression does not match actual output format | Check Agent output for quotes/newlines; try `!= ""` first |
-| No final output | Output node branch not reached | Verify condition wiring; ensure every path reaches an **Output** node |
-| Role chat does not run workflow | Role not bound or disabled | Check `workflow_id`, `workflow_policy: auto`, workflow `enabled: true` |
-| Tool node fails | Invalid JSON in arguments or tool disabled | Fix argument template; enable the tool in MCP settings |
-| Save fails with invalid branch | Condition outgoing edges are not marked yes/no, or are duplicated | Select the edge and set branch to `true` or `false` |
-| Multi-upstream result is unexpected | Join strategy does not match the workflow | Switch between `all_merge`, `first_non_empty`, `last_by_canvas`, and `fail_fast` |
-| Nested field is empty | JSONPath/JQ path is outside the safe subset | Use `$.a.b[0]` or `.a.b[0]`; avoid wildcards, recursion, or expressions |
-
----
-
-## 12. Best practices
-
-1. **Meaningful names**: Use descriptive output variable names (`scan_result`, `parsed_targets`) instead of reusing `agent_result` everywhere.
-2. **Prefer `outputs` for cross-node data**: If a condition, tool, or HITL node might sit in between, use named variables.
-3. **Use `previous` only for direct links**: `A → B` with nothing in between is the ideal case for `{{previous.output}}`.
-4. **Conditions should reference source data**: When testing Agent output, use `{{outputs.xxx}}` unless the condition immediately follows that Agent.
-5. **Every path needs an exit**: Ensure both yes and no branches eventually reach an **Output** node (or your intended end).
-6. **Choose join strategy explicitly for multi-upstream nodes**: Use `all_merge` for aggregation, `first_non_empty` for fallback, and `fail_fast` for critical gates.
-7. **Use JSONPath/JQ safe paths for nested JSON**: e.g. `jsonpath({{previous.output}}, "$.status") == "ok"`.
-8. **Dry-run before real execution**: Validate data flow and branches with a simple message before binding the workflow to a role.
-
----
-
-## 13. Code references (for developers)
-
-| Module | Path |
-|--------|------|
-| Execution engine | `internal/workflow/runner.go` |
-| Eino compile / checkpoint / HITL | `internal/workflow/eino_compile.go` |
-| Graph validation | `internal/workflow/validation.go` |
-| Expressions / JSONPath / joins | `internal/workflow/expression.go`, `jsonpath.go`, `join.go` |
-| Dry-run / replay data | `internal/workflow/dry_run.go`, `internal/handler/workflow_run.go` |
-| Canvas UI | `web/static/js/workflows.js` |
-| Workflow API | `internal/handler/workflow.go` |
-| Role binding | `internal/config/config.go` (`workflow_id` field) |