Files
easynode/AI-Agent.md
T
2026-08-02 16:09:21 +08:00

748 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EasyNode AI Agent 架构与时序
本文描述 EasyNode 当前 AI Agent 的实现架构、主要数据流、权限边界和关键时序。
## 1. 架构目标
AI Agent 不是由浏览器直接拼接历史并调用模型,而是采用“服务端持有会话状态、WebSocket 驱动单轮任务、模型通过受控工具操作主机”的架构。
核心原则:
- 会话历史以服务端数据库为准,前端每轮只提交本次输入。
- REST 负责会话 CRUD,WebSocket 负责实时运行、审批和中断。
- 模型看到当前作用域和主机选择允许的完整工具集;执行模式与 Plus 权限在每次调用时决定自动执行、审批或拒绝。
- 主机必须由用户在本轮请求中显式选择,空集合表示纯聊天模式。
- 会话权限与主机策略取交集,且每次工具调用都重新校验。
- 命令执行前依次经过访问控制、风险识别和人工审批。
- 普通运维 Agent 使用独立 SSH exec/SFTP 通道,终端 AI 使用当前 Web 终端 PTY。
- 工具调用、审批结果、风险信息和部分失败结果都进入会话与审计记录。
## 2. 总体架构
```mermaid
flowchart TB
subgraph Web["Web 前端"]
Entry["全局 AI 入口<br/>ai-agent/index.vue"]
TerminalUI["终端 AI 侧栏<br/>terminal-ai-chat.vue"]
SessionState["会话状态<br/>useAgentSession.js"]
EventReducer["事件归约与消息渲染<br/>agentMessages.js"]
Terminal["当前 Web Terminal<br/>terminal.vue"]
end
subgraph Transport["传输层"]
REST["REST /api/v1/agent-sessions"]
AgentWS["Socket.IO /ai-agent"]
TerminalWS["Socket.IO /terminal"]
end
subgraph Server["服务端 AI Agent"]
SocketGateway["Agent Socket 网关<br/>socket/ai-agent.js"]
Runtime["Agent Runtime<br/>ai/runtime.js"]
Provider["模型适配<br/>ai/provider.js"]
Prompt["System Prompt<br/>ai/prompt.js"]
Policy["权限策略<br/>ai/policy.js"]
Safety["Shell 解析与风险分类<br/>ai/shell-lexer.js<br/>ai/safety.js"]
DataPolicy["敏感数据与文件目标<br/>ai/data-policy.js<br/>ai/file-mutation-policy.js"]
Approval["审批网关<br/>ai/approval.js"]
WriteGuard["写入预览与唯一备份<br/>ai/write-preview.js<br/>ai/file-backup.js"]
ToolRegistry["工具注册<br/>ai/tools/spec.js"]
Executors["工具执行器<br/>ai/tools/executors.js"]
HostAccess["逐主机访问控制<br/>ai/host-access.js"]
SSH["SSH/SFTP 连接池<br/>ai/ssh.js"]
TerminalDispatch["终端命令调度<br/>ai/terminal-dispatch.js"]
SessionStore["会话存储<br/>ai/session-store.js"]
Compaction["上下文压缩<br/>ai/compaction.js"]
OutputStore["长输出缓存<br/>ai/output-store.js"]
Audit["安全审计<br/>ai/audit.js"]
end
subgraph Data["数据与外部系统"]
AgentDB[("agent-session.db")]
ConfigDB[("ai-config.db")]
HostDB[("host.db")]
ModelAPI["OpenAI-compatible / Anthropic / Google"]
RemoteHost["远程 Linux 主机"]
end
Entry --> SessionState
TerminalUI --> SessionState
SessionState --> EventReducer
SessionState --> REST
SessionState <--> AgentWS
AgentWS <--> SocketGateway
REST <--> SessionStore
SocketGateway --> Runtime
Runtime --> Provider
Runtime --> Prompt
Runtime --> Policy
Runtime --> Safety
Runtime --> DataPolicy
Runtime --> Approval
Runtime --> WriteGuard
Runtime --> Audit
Runtime --> ToolRegistry
Runtime --> SessionStore
Runtime --> Compaction
Provider --> ConfigDB
Provider <--> ModelAPI
SessionStore <--> AgentDB
Runtime --> HostDB
ToolRegistry --> Executors
Safety --> DataPolicy
Approval --> Audit
Executors --> HostAccess
HostAccess --> HostDB
Executors --> WriteGuard
WriteGuard --> SSH
Executors --> OutputStore
Executors --> Audit
Executors --> SSH
SSH <--> RemoteHost
Executors --> TerminalDispatch
TerminalDispatch --> SocketGateway
SocketGateway --> SessionState
SessionState --> Terminal
Terminal <--> TerminalWS
TerminalWS <--> RemoteHost
```
## 3. 分层职责
| 层 | 主要文件 | 职责 |
| --- | --- | --- |
| 前端容器 | `web/src/components/ai-agent/index.vue` | 全局运维助手、主机选择、模型和模式切换 |
| 终端容器 | `web/src/views/terminal/components/terminal-ai-chat.vue` | 绑定当前终端主机,接收和转发终端命令 |
| 前端会话层 | `web/src/composables/useAgentSession.js` | Socket 生命周期、会话状态、审批、中断、REST 会话操作 |
| 前端事件层 | `web/src/composables/agentMessages.js` | 将流式事件归约为文本、推理块和工具卡片 |
| Socket 网关 | `server/app/socket/ai-agent.js` | 鉴权、任务互斥、创建会话、启动 Runtime、最终落盘 |
| Runtime | `server/app/ai/runtime.js` | 模型调用、统一操作分类、审批决策、工具循环、事件映射和超限重试 |
| Provider | `server/app/ai/provider.js` | 将不同模型供应商适配为统一 AI SDK 模型 |
| 工具层 | `server/app/ai/tools/` | 工具元数据、Zod Schema、权限过滤和具体执行 |
| 安全层 | `policy.js`、`host-access.js`、`safety.js`、`data-policy.js`、`file-mutation-policy.js`、`approval.js` | 权限交集、主机边界、Shell/数据风险判定和人工审批 |
| 文件保护层 | `write-preview.js`、`file-backup.js`、`remote-path.js` | 真实路径识别、完整差异、执行前快照复核和不可覆盖备份 |
| 执行层 | `ssh.js`、`terminal-dispatch.js`、`socket/terminal.js` | 独立 SSH/SFTP 执行或当前 PTY 执行 |
| 持久化层 | `session-store.js`、`compaction.js` | 会话 CRUD、消息修复、摘要压缩、Token 统计 |
## 4. 两种 Agent 作用域
### 4.1 运维助手 `scope=ops`
- 可以选择零到多台主机。
- 未选择主机时不注册任何主机工具,退化为纯聊天。
- 选择主机后向模型开放运维工具集;执行模式不删减工具,具体调用按目标主机策略重新判定。
- 命令通过服务端独立 SSH exec channel 执行。
- 文件操作通过服务端 SFTP 执行。
### 4.2 终端助手 `scope=terminal`
- 必须绑定且只能绑定一台主机。
- 历史会话不能跨主机加载。
- 模型只获得 `terminal_command` 和 `read_output` 工具。
- `terminal_command` 不创建新的 SSH exec channel,而是发送到用户当前打开的 PTY。
- 当前实现发送消息时只附带终端连接和主机信息,不默认把整个终端滚动缓冲区写入模型历史。
- 通过 AI 执行的命令输出以结构化 Tool Result 进入会话历史。
## 5. 普通运维 Agent 主时序
```mermaid
sequenceDiagram
autonumber
actor User as 用户
participant UI as Vue AI Agent
participant WS as /ai-agent
participant Gateway as Socket Gateway
participant Store as Session Store
participant Runtime as Agent Runtime
participant Provider as Model Provider
participant Model as LLM API
participant Tools as Tool Layer
participant SSH as SSH/SFTP Pool
participant Host as 远程主机
User->>UI: 输入任务并选择模型、权限和主机
UI->>WS: ws_agent_run(sessionId?, input, modelId, permission, hostIds, scope=ops)
WS->>Gateway: 接收并校验请求
alt 没有现有会话
Gateway->>Store: createSession(...)
Store-->>Gateway: 新会话
Gateway-->>UI: agent_event(session_created)
else 继续现有会话
Gateway->>Store: getSession(sessionId)
Store-->>Gateway: 会话元数据
end
Gateway->>Runtime: runTurn(...)
Runtime->>Provider: resolveModel(modelId)
Provider-->>Runtime: model、contextLimit、maxSteps
Runtime->>Runtime: 计算权限交集并构造 Tools/System Prompt
Runtime->>Store: loadForModel(sessionId)
Store-->>Runtime: 修复/压缩后的模型历史
Runtime-->>UI: turn_start(policy, availableTools)
Runtime->>Model: streamText(system, history + input, tools)
loop 最多 maxSteps 次模型/工具循环
Model-->>Runtime: reasoning/text/tool-call
Runtime-->>UI: reasoning_delta / text_delta / tool_call
opt 模型调用工具
Runtime->>Tools: 校验、审批并执行 Tool
Tools->>SSH: acquire(hostId)
SSH->>Host: SSH exec 或 SFTP 操作
Host-->>SSH: stdout/stderr/exitCode 或文件结果
SSH-->>Tools: 结构化执行结果
Tools-->>Runtime: 脱敏、截断后的 Tool Result
Runtime-->>UI: tool_progress / tool_result
Runtime->>Model: 将 Tool Result 送回模型
Runtime-->>UI: awaiting_model / model_resumed
end
end
Model-->>Runtime: finish + usage + responseMessages
Runtime-->>UI: finish
Runtime-->>Gateway: 本轮完整或部分结果
Gateway->>Store: appendTurn(user + responseMessages + toolMeta + usage)
Store-->>Gateway: 更新后的会话
Gateway-->>UI: session_saved
```
### 5.1 Socket 网关的落盘保证
`appendTurn` 位于 Socket 网关的 `finally` 中。即使发生以下情况,本轮已产生的数据也会尽量保存:
- 用户主动停止;
- 工具执行失败;
- 模型流中途失败;
- 审批被拒绝或超时;
- 已经执行了工具,但模型没有生成最终回复。
这样可以避免“远端命令已经执行,但会话中没有记录”的情况。
## 6. 工具调用与审批时序
```mermaid
sequenceDiagram
autonumber
participant Model as LLM
participant Runtime as Runtime toolApproval
participant Access as Host Access
participant Safety as Safety
participant Approval as Approval Gateway
participant UI as Approval UI
participant Executor as Tool Executor
participant Audit as Audit Log
Model->>Runtime: tool-call(name, input)
Runtime->>Access: resolveHostAccess(hostId)
Access->>Access: 检查本轮主机集合、主机开关并解析生效策略
alt 主机或能力不允许
Access-->>Runtime: HostAccessError
Runtime->>Audit: DENIED
Runtime-->>UI: tool_denied
Runtime-->>Model: denied(reason)
else 访问允许
Runtime->>Safety: 统一准备 effect/risk/targets/preview
Safety-->>Runtime: 分类结果与执行前快照
alt 风险级别为 deny
Runtime->>Audit: DENIED
Runtime-->>UI: tool_denied
Runtime-->>Model: denied(reason)
else 超出主机 maxEffect
Runtime->>Audit: DENIED
Runtime-->>UI: tool_denied
Runtime-->>Model: denied(reason)
else 不需要审批
Runtime->>Runtime: 记录已授权快照/路径/脚本哈希
Runtime->>Executor: execute(input)
Executor->>Audit: TOOL_CALL / EXEC
Executor-->>Runtime: result
Runtime-->>Model: tool-result
else 需要人工审批
Runtime->>Approval: requestApproval(...)
Approval-->>UI: approval_request
UI->>Approval: ws_agent_approve(approved, scope)
alt 用户允许
Approval->>Audit: APPROVED
Approval-->>Runtime: approved
Runtime->>Runtime: 记录已批准快照/路径/脚本哈希
Runtime->>Executor: execute(input)
Executor-->>Runtime: result
Runtime-->>Model: tool-result
else 用户拒绝或五分钟超时
Approval->>Audit: REJECTED
Approval-->>Runtime: denied(reason)
Runtime-->>Model: denied(reason)
end
end
end
```
注意:
- `deny` 是硬拦截,不向用户提供继续执行按钮。
- `high` 即使在“授权”模式也必须逐次确认。
- Runtime 是分类和审批的唯一决策点;Executor 只消费已准备的分类结果,并复核文件快照、敏感读取真实路径或脚本哈希等会发生变化的对象。
- `run_script` 按实际脚本内容分类并复核内容哈希;普通静态脚本遵循当前模式矩阵。
- 终端命令遵循同一模式矩阵:审查模式下所有主机操作需审批,其他模式按操作和风险判定。
- 仅协助模式下 `normal/write` 且静态、单段、无重定向的少数服务生命周期操作支持“本会话允许同一操作”。当前仅覆盖 `systemctl`、`service`、`rc-service`、`docker` 和 `podman` 的明确 `start/restart/reload` 类操作。
- 会话授权指纹包含工具、主机和完整命令参数;文件写入、终端命令、复合/动态命令、删除和 `high` 操作始终单次审批。
## 7. 终端 AI 命令时序
终端 AI 涉及两条 Socket:
- `/ai-agent`:模型运行和 Agent 事件。
- `/terminal`:现有 WebSSH PTY 输入输出。
```mermaid
sequenceDiagram
autonumber
actor User as 用户
participant AIUI as 终端 AI 侧栏
participant AgentWS as /ai-agent
participant Runtime as Agent Runtime
participant Dispatch as Terminal Dispatch
participant TerminalVue as terminal.vue
participant TerminalWS as /terminal
participant Bridge as Terminal AI Bridge
participant PTY as 远端 PTY
User->>AIUI: 输入终端相关任务
AIUI->>AIUI: 验证当前终端已连接
AIUI->>AgentWS: ws_agent_run(scope=terminal, hostId, terminalContext)
AgentWS->>Runtime: runTurn(...)
Runtime->>Runtime: 仅注册 terminal_command/read_output
Runtime->>Dispatch: requestTerminalDispatch(command)
Dispatch-->>AIUI: terminal_command_request(requestId, command)
AIUI->>TerminalVue: executeAiCommand(command)
TerminalVue->>TerminalWS: ai_terminal_command(localRequestId, command)
TerminalWS->>Bridge: 构造随机 begin/end marker
Bridge->>PTY: 写入 Base64 包装命令
PTY-->>Bridge: 命令回显、持续输出、结束 marker 和退出码
Bridge->>Bridge: 过滤包装器和 marker,截取本次命令输出
Bridge-->>TerminalVue: terminal_ai_command_progress
TerminalVue-->>AIUI: reportProgress(...)
AIUI->>AgentWS: ws_terminal_command_progress
AgentWS->>Dispatch: reportTerminalDispatchProgress
Dispatch-->>AIUI: agent_event(terminal_command_progress)
PTY-->>Bridge: end marker
Bridge-->>TerminalVue: terminal_ai_command_result
TerminalVue-->>AIUI: Promise 完成
AIUI->>AgentWS: ws_terminal_command_result
AgentWS->>Dispatch: resolveTerminalDispatch
Dispatch-->>Runtime: output、exitCode、durationMs
Runtime->>Runtime: 生成 Tool Result 并继续模型循环
```
### 7.1 PTY 命令边界
当前 PTY 是交互式流,不能天然知道某条命令何时结束。服务端终端桥接层会:
1. 为每条 AI 命令生成随机 token。
2. 将原命令 Base64 编码,避免直接插入包装脚本时发生转义问题。
3. 在命令前后输出随机 `begin/end` marker。
4. 从 PTY 数据流中隐藏包装器和 marker。
5. 只收集 begin/end 之间的输出。
6. 从 end marker 读取退出码。
同一个终端同一时间只允许一条 AI 命令执行,单条命令最长等待 60 分钟。
## 8. 停止与断连时序
```mermaid
sequenceDiagram
autonumber
actor User as 用户
participant UI as Vue
participant WS as /ai-agent
participant Gateway as Socket Gateway
participant Runtime as Runtime
participant Approval as Approval
participant Dispatch as Terminal Dispatch
participant SSH as SSH/PTY
participant Store as Session Store
User->>UI: 点击停止
UI->>WS: ws_agent_stop
WS->>Gateway: stop
Gateway->>Runtime: AbortController.abort()
Gateway-->>UI: stopped
par 清理审批
Runtime->>Approval: AbortSignal
Approval-->>UI: approval_cancelled
and 中断普通 SSH 命令
Runtime->>SSH: kill/close exec stream
and 中断终端命令
Runtime->>Dispatch: AbortSignal
Dispatch-->>UI: terminal_command_cancel
UI->>SSH: /terminal 发送 Ctrl-C
end
Runtime-->>UI: aborted
Gateway->>Store: appendTurn(已产生的部分消息)
Gateway-->>UI: session_saved
```
Socket 断连时还会:
- 中止当前 `AbortController`;
- 清理该会话挂起的审批;
- 清理终端调度请求;
- 清理该会话的长输出缓存;
- 普通运维作用域主动释放使用过的 Agent SSH 连接;
- 前端将仍在运行或等待审批的工具卡片标记为失败,避免永久转圈。
## 9. 上下文加载与压缩时序
```mermaid
sequenceDiagram
autonumber
participant Runtime as Runtime
participant Store as Session Store
participant Compact as Compaction
participant Model as LLM
participant DB as agent-session.db
participant UI as Vue
Runtime->>Store: loadForModel(sessionId, model, contextLimit)
Store->>DB: 读取完整会话
DB-->>Store: messages + compaction
Store->>Compact: 估算未覆盖历史的 Token
alt 达到上下文预算约 70%
Compact->>Model: 生成旧历史摘要
Model-->>Compact: summary
Compact-->>Store: summary + splitAt
Store->>DB: 保存 compaction(summary, upTo)
Store-->>Runtime: compactedNow
Runtime-->>UI: compacted
end
Store->>Store: 修复孤立 tool-call/tool-result
Store->>Store: 按完整轮次执行硬裁剪
Store-->>Runtime: 摘要 + 最近原始消息
Runtime->>Model: 发起本轮 streamText
alt Provider 返回上下文超限
Runtime->>Compact: 强制压缩,保留最近一轮
Compact->>Model: 生成应急摘要
Model-->>Runtime: 压缩结果
Runtime-->>UI: compacted
Runtime->>Model: 整轮重试一次
end
```
上下文策略:
- 默认上下文预算为 64K Token,可由 AI 配置中的 `contextLimit` 覆盖。
- 估算达到预算的约 70% 时主动压缩。
- 正常压缩保留最近三轮原文。
- 厂商仍返回上下文超限时,强制压缩并仅重试一次。
- 摘要以 `{ summary, upTo }` 单独保存;原始消息在硬上限内仍可供前端查看。
- 单会话硬上限为 200 条消息或约 1.5 MiB,裁剪只从完整对话轮次边界进行。
- 中断导致的无结果 Tool Call 会补充合成错误结果,防止后续模型请求因消息不配对返回 400。
## 10. 权限模型
### 10.1 执行模式
每次操作统一分类为 `read | write | delete`,风险分为 `normal | high | deny`。
只有能够明确证明只读的 Shell 命令归为 `read`,明确删除归为 `delete`,其他静态命令默认归为 `normal/write`。因此新增普通 CLI 无需加入安全白名单;动态构造以及明确命中的高危、永久拒绝操作仍受负向规则约束。
| 预设 | 普通读取 | 高危读取 | 普通写入 | 高危写入 | 删除 |
| --- | --- | --- | --- | --- | --- |
| 审查 `review` | 主机读取审批;本地元数据自动 | 审批 | 审批 | 审批 | 审批 |
| 协助 `assist` | 自动 | 审批 | 审批 | 审批 | 审批 |
| 授权 `authorized` | 自动 | 审批 | 自动 | 审批 | 普通自动、高危审批 |
面向用户的模式描述为:审查模式下所有主机操作均需确认;协助模式仅明确只读的操作自动执行;授权模式自动执行未命中高危规则的操作。永久禁止的操作在所有模式下始终拦截。
### 10.2 主机策略
每台主机可配置:
```js
{
enabled: true,
maxEffect: 'write',
maxMode: 'authorized'
}
```
最终生效策略:
```text
生效 mode = min(会话权限模式, 主机 maxMode)
写入和删除仅在 maxEffect = write 时允许
```
多主机会话展示工具并集,真正执行时按目标主机重新判定。
### 10.3 Shell 分类收敛
Shell 安全策略采用开放式授权模型,不维护“已知安全命令白名单”。解析层先拆分命令段、参数、重定向、管道、包装器和嵌套负载,分类层统一返回:
```js
{
effect, // read | write | delete
risk, // normal | high | deny
category,
reason,
targets,
traits, // recursive | force | bulk | dynamic | sensitive | external | hidden | opaque
hits,
segments
}
```
判定规则:
1. 明确具有删除语义的操作归为 `delete`,包括文件删除或移动源、`truncate`、包/账号/数据库/容器/Kubernetes 删除等。
2. 只有静态且能够明确证明只读的命令归为 `read`。
3. 其他静态命令,包括尚未登记的新 CLI,默认归为 `normal/write`。
4. 变量、命令替换、`eval` 和无法静态确认的解释器代码归为 `high`,而不是因为命令名未知而升级。
5. 复合命令逐段分类并取最严格的 `effect` 和 `risk`;`sh -c`、`su -c`、远程 `ssh` 命令及 `systemd-run` 的静态负载会递归分析。
6. 镜像名、包名、URL、文件内容或普通目标中的产品关键词不会改变风险等级。
因此 `future-cli deploy app` 这类未知静态命令表现为:审查、协助模式审批,授权模式自动执行。新增普通 CLI 不需要补安全规则,只有新增的明确高危或永久拒绝机制需要进入负向规则集。
### 10.4 风险边界
| 等级 | 主要类别 | 行为 |
| --- | --- | --- |
| `deny` | `dd`;文件系统格式化/擦除;块设备直接写入;根目录或关键系统目录树整体销毁;fork bomb;递归锁死或完全放开关键系统目录权限;核心凭据外传;Base64 等隐藏载荷直接交给解释器执行 | 所有模式永久拒绝,只允许向用户展示原始命令和原因,不得自动改写、拆分或绕过 |
| `high` | 敏感读取/写入;强制、递归或批量删除;SSH、防火墙、网卡、账号和提权变更;关机重启;包卸载;数据库清空;容器强制删除、清理和卷删除;Kubernetes 命名空间或持久卷删除;动态/间接执行;文件外传;下载后执行 | 所有模式均需单次审批 |
| `normal` | 未命中上述负向规则的静态读取、写入和删除 | 按执行模式矩阵处理 |
该分类器是防误操作护栏,不是恶意命令沙箱;真正的权限边界仍是主机策略、目标主机集合和 SSH 账号自身权限。
## 11. 工具清单
| 工具 | 操作类型 | Plus 策略 | 执行通道 | 特殊规则 |
| --- | --- | --- | --- | --- |
| `host_list` | read | 免费 | NeDB | 只返回本轮显式选择的主机 |
| `host_status` | read | 免费 | SSH exec | 返回结构化系统状态 |
| `script_list` | read | 免费 | NeDB/脚本库 | 返回内置和用户脚本 |
| `run_script` | 按内容判定 | 写入/删除需 Plus | SSH exec | 按脚本内容分类并校验哈希 |
| `exec_command` | 按内容判定 | 写入/删除需 Plus | SSH exec | 经过统一 Shell 分类 |
| `read_file` | read | 免费 | SFTP | 敏感读取需审批 |
| `write_file` | write | Plus | SFTP | 默认备份,高危路径展示完整 diff |
| `list_dir` | read | 免费 | SFTP | 返回类型、大小、权限和时间 |
| `read_output` | read | 免费 | 内存缓存 | 回读被截断的工具输出 |
| `terminal_command` | 按内容判定 | 写入/删除需 Plus | 当前 Web PTY | 按实际命令分类 |
工具规格是模型 Schema、System Prompt 和前端能力展示的共同数据源,避免多处声明发生漂移。
### 11.1 文件与脚本执行不变量
- `read_file` 在审批前解析真实路径,并同时检查请求路径与真实路径。敏感读取执行时必须匹配同一次工具调用批准的真实路径。
- `write_file` 仅接受绝对路径、有效 UTF-8 文本和最多 256 KiB 的完整内容;权限值只能是 3 或 4 位八进制数字。
- 写入前读取原文件并生成完整替换 diff,快照哈希覆盖主机、请求路径、真实路径、原内容、权限、备份选项和新内容。执行前重新生成快照,任何变化都会使原授权失效。
- 覆盖已有文件时默认创建 `${原路径}.bak.<UTC 时间戳>`;同一毫秒或并发碰撞时追加序号,并使用 SFTP 独占创建,绝不覆盖已有备份。
- `run_script` 只执行脚本库返回的原始内容。Runtime 按实际脚本分类并记录 SHA-256,Executor 执行前重新读取脚本并校验哈希。
- Executor 不重新解释不可变命令,只消费 Runtime 已保存的 `effect/risk`;仅保留文件快照、敏感读取真实路径和脚本哈希这类 TOCTOU 复核。
## 12. 长输出与敏感信息
### 12.1 长输出
- SSH 命令单次最多收集约 2 MiB,超过后标记为截断。
- 工具结果超过约 8 KiB 时,返回头部、尾部和临时 `handle`。
- 模型可调用 `read_output(handle, pattern, offset, limit)` 分段回读。
- 临时输出缓存默认保留 30 分钟,最多 200 项,并在会话断开时清理。
### 12.2 脱敏
普通工具结果返回模型前会自动脱敏。高危读取经过用户单次批准后,真实内容会发送给当前 AI Provider;未经审批的意外凭据输出仍会脱敏。核心凭据的明确外传命令直接归为 `deny`。
长输出 `handle` 与创建它的会话绑定,其他会话不能回读;存入缓存的是当前工具调用经过脱敏策略处理后的内容。
## 13. 模型与运行配置
运维助手与终端助手共用 AI 设置:
| 配置 | 说明 |
| --- | --- |
| `providerType` | `openai-compatible`、`anthropic` 或 `google` |
| `apiUrl` / `apiKey` | Provider 的 API 前缀与凭据 |
| `models` | 允许在 Agent 中选择的模型 ID 列表;OpenAI-compatible Provider 支持从接口获取候选模型 |
| `contextLimit` | 模型上下文窗口,默认 65,536 Token |
| `maxSteps` | 单轮最多模型/工具迭代次数,默认 25,服务端上限 50 |
Provider 层将三类服务适配为统一的 AI SDK 模型。请求中的模型 ID 必须位于已配置列表中;未显式选择时使用列表第一项。
## 14. 会话数据模型
`agent-session.db` 中单条记录的主要结构:
```js
{
_id,
title,
scope, // ops | terminal
hostId, // terminal 会话绑定主机
hostIds, // 本会话主机集合
modelId,
permission,
messages, // AI SDK ModelMessage[]
turnMeta: [
{
createdAt,
usage
}
],
toolMeta: {
[toolCallId]: {
tool,
effect,
risk,
riskReason,
riskCategory,
targets,
approved,
approvalScope,
approvalCached,
denied,
sensitiveDisclosure,
durationMs,
failed
}
},
usage,
compaction: {
summary,
upTo,
createdAt
},
createdAt,
updatedAt
}
```
会话列表接口只返回摘要字段,不返回可能达到数百 KiB 的 `messages`。完整历史通过详情接口按需加载。
Agent 会话不按时间自动清理,仅支持用户主动删除单个会话或清空会话。
## 15. 接口与事件协议
### 15.1 REST
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/v1/agent-sessions` | 按 scope/hostId 获取会话列表 |
| `DELETE` | `/api/v1/agent-sessions` | 按作用域清空会话 |
| `GET` | `/api/v1/agent-sessions/:id` | 获取完整会话 |
| `PUT` | `/api/v1/agent-sessions/:id` | 更新标题、主机、模型或权限 |
| `POST` | `/api/v1/agent-sessions/:id/fork` | 从指定问答或回答创建分支 |
| `PUT` | `/api/v1/agent-sessions/:id/messages/:turnIndex` | 截断旧分支并准备重新发送编辑后的消息 |
| `DELETE` | `/api/v1/agent-sessions/:id` | 删除会话 |
### 15.2 客户端发送到 `/ai-agent`
| 事件 | 主要载荷 | 用途 |
| --- | --- | --- |
| `ws_agent_run` | `sessionId?`、`input`、`modelId`、`permission`、`hostIds`、`scope`;终端作用域另带 `hostId`、`terminalPermission`、`terminalContext` | 启动一轮任务 |
| `ws_agent_approve` | `requestId`、`approved`、`scope` | 回复审批 |
| `ws_agent_stop` | 无 | 中止当前任务 |
| `ws_terminal_command_progress` | `requestId`、`output`、`durationMs` | 回传 PTY 命令进度 |
| `ws_terminal_command_result` | `requestId`、`output`、`exitCode` | 回传 PTY 最终结果 |
### 15.3 服务端统一发送 `agent_event`
主要 `type`:
| 类别 | type |
| --- | --- |
| 初始化 | `ready`、`session_created`、`turn_start`、`pending_approvals` |
| 模型流 | `reasoning_delta`、`text_delta`、`step_finish`、`finish` |
| 工具 | `tool_call`、`tool_progress`、`tool_result`、`tool_denied`、`tool_requires_plus` |
| 模型等待 | `awaiting_model`、`model_resumed` |
| 审批 | `approval_request`、`approval_timeout`、`approval_cancelled` |
| 终端 | `terminal_command_request`、`terminal_command_progress`、`terminal_command_cancel` |
| 上下文 | `history_repaired`、`compacted` |
| 生命周期 | `session_saved`、`aborted`、`stopped`、`error`、`stream_error` |
## 16. 关键实现文件
```text
server/app/
├── ai/
│ ├── runtime.js # 单轮 Agent 编排
│ ├── provider.js # 模型供应商适配
│ ├── prompt.js # System Prompt
│ ├── policy.js # 双轴权限模型
│ ├── host-access.js # 逐主机执行边界
│ ├── safety.js # Shell 风险分类
│ ├── shell-lexer.js # Shell 词法解析
│ ├── data-policy.js # 敏感读取路径分类
│ ├── file-mutation-policy.js # Shell 文件变更目标提取
│ ├── write-preview.js # write_file 完整差异与快照
│ ├── file-backup.js # 唯一且不可覆盖的备份
│ ├── remote-path.js # 远程真实路径解析
│ ├── approval.js # 人工审批挂起/恢复
│ ├── audit.js # 安全审计
│ ├── redact.js # 输出脱敏
│ ├── ssh.js # Agent SSH/SFTP 连接池
│ ├── terminal-dispatch.js # 当前 PTY 调度
│ ├── output-store.js # 长输出缓存
│ ├── compaction.js # 上下文摘要
│ ├── session-store.js # Agent 会话持久化
│ ├── plus.js # 加密后的 Plus 变更工具实现
│ └── tools/
│ ├── spec.js # 工具元数据与 Schema
│ ├── index.js # AI SDK Tool 装配
│ └── executors.js # 工具执行器
├── script-library.js # 内置/用户脚本统一读取
├── controller/
│ └── agent-session.js # 会话 REST Controller
└── socket/
├── ai-agent.js # Agent WebSocket 网关
└── terminal.js # PTY AI 命令边界协议
web/src/
├── components/ai-agent/ # 全局 Agent、审批卡、工具卡、会话列表和模式切换
├── components/common/
│ └── chat-sender.vue # 运维/终端助手共用输入框
├── composables/
│ ├── useAgentSession.js # 前端会话与 Socket 状态
│ ├── agentMessages.js # Agent 事件归约
│ └── agentTools.js # 前端工具能力展示
└── views/terminal/components/
├── terminal-ai-chat.vue # 终端 AI 侧栏
└── terminal.vue # PTY 命令发送、进度和取消
```
## 17. 验证覆盖
暂存区实现包含以下自动化验证:
- `server/test/test-ai-safety.js`:三档模式矩阵、Shell 复合命令、嵌套负载、路径变体、文件目标、高危和永久拒绝规则。
- `server/test/test-ai-access.js`:主机隔离、纯聊天模式、Shell 转义、审批指纹、终端取消和执行步数上限。
- `server/test/test-ai-data.js`:敏感路径、脱敏、写入预览、快照与唯一备份。
- `server/test/test-ai-session.js`:消息修复、会话 CRUD、编辑、分支、持久化和用量累计。
- `server/test/test-ai-compaction.js`:Token 估算、摘要切分、主动/应急压缩和上下文超限识别。
- `web/test/test-agent-messages.js`:流式事件归约、工具卡、历史还原、回答重生成和不同事件来路的一致性。
常用校验命令:
```bash
yarn workspace server run test:ai
yarn workspace server run lint
yarn workspace web run test
yarn workspace web run lint
yarn workspace web run build
git diff --check
```