From e1ac6733e81c6e0ceed591cb1c4ddf3d72fd9f91 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Fri, 27 Mar 2026 00:15:37 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20openspec=20?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - abp.md: ABP (AstrBot Plugin) 插件协议规范 - api.md: API 规范 - agent-message.md: Agent 消息格式规范 - config.md: 配置系统规范 (含 GPG 安全配置) - env.md: 环境变量规范 - path.md: XDG 路径规范 --- openspec/abp.md | 415 ++++---- openspec/agent-message.md | 2127 +++++++++++++++++++++++++++++++++++++ openspec/api.md | 651 ++++++++++++ openspec/config.md | 565 ++++++++++ openspec/env.md | 114 ++ openspec/path.md | 76 +- 6 files changed, 3743 insertions(+), 205 deletions(-) create mode 100644 openspec/agent-message.md create mode 100644 openspec/api.md create mode 100644 openspec/config.md diff --git a/openspec/abp.md b/openspec/abp.md index ac9dbfc18..8506f4d4d 100644 --- a/openspec/abp.md +++ b/openspec/abp.md @@ -5,7 +5,10 @@ ABP 是 AstrBot 的插件通信协议,用于插件的注册、消息处理、工具调用和生命周期管理。 ABP 采用**插件作为独立服务**的设计,插件通过配置加载,可使用任意编程语言实现。 -**核心定位**:ABP 定义了 AstrBot 编排器与插件之间的交互接口,支持进程内和跨进程两种加载模式。 +**核心原则**: +- **主进程不读取插件配置文件**:插件配置通过协议获取 +- **数据目录由主进程分配**:插件的数据存储目录由主进程在握手时告知 +- **零侵入性**:插件无需了解 AstrBot 内部结构 ## 与 MCP 的关系 @@ -13,46 +16,22 @@ ABP 采用**插件作为独立服务**的设计,插件通过配置加载,可 |------|-----|-----| | 定位 | 插件(功能扩展) | 工具/数据源连接 | | 通信模式 | 双向(消息+工具+事件) | 单向(工具调用+资源访问) | +| 配置获取 | 通过协议握手获取 | 通过配置文件声明 | +| 数据目录 | 主进程分配 | 插件自行管理 | | 消息处理 | 支持 | 不支持 | -| 生命周期 | 完整 | 轻量 | -| 编程语言 | 任意 | 任意 | -## 加载方式 +## 插件加载配置 -### 1. 进程内加载(In-Process) - -- **直接函数调用**:无序列化开销,零拷贝 -- **适用场景**:内置插件、高频调用、对延迟敏感 - -### 2. 跨进程加载(Out-of-Process) - -- **独立进程运行**:插件崩溃不影响主进程 -- **协议**:JSON-RPC 2.0 over Unix Socket / HTTP -- **适用场景**:第三方插件、需要资源隔离 - -## 插件配置 - -类似 MCP 服务器配置,在 AstrBot 配置文件中声明插件: +AstrBot 配置文件声明插件基本信息(不包含插件配置): ```json { "plugins": [ { "name": "weather-plugin", - "version": "1.0.0", "load_mode": "out_of_process", "command": "python", - "args": ["/path/to/weather_server.py"], - "env": { - "API_KEY": "xxx" - } - }, - { - "name": "code-analysis", - "load_mode": "out_of_process", - "command": "./code-analysis-server", - "transport": "http", - "url": "http://localhost:9001" + "args": ["/path/to/weather_server.py"] } ] } @@ -63,20 +42,183 @@ ABP 采用**插件作为独立服务**的设计,插件通过配置加载,可 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `name` | string | 是 | 插件名称 | -| `version` | string | 否 | 插件版本 | | `load_mode` | string | 是 | `in_process` 或 `out_of_process` | | `command` | string | 跨进程时必填 | 启动命令 | | `args` | array | 跨进程时可选 | 命令参数 | -| `env` | object | 否 | 环境变量 | | `transport` | string | 跨进程时可选 | `stdio` / `unix_socket` / `http` | | `url` | string | HTTP 传输时必填 | 服务器地址 | +## 初始化握手 + +插件配置通过 `initialize` 握手交换,**主进程不读取插件的配置文件**。 + +### Initialize(主进程 → 插件) + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "1.0.0", + "clientInfo": { + "name": "astrbot", + "version": "4.16.0" + }, + "capabilities": { + "streaming": true, + "events": true + }, + "pluginConfig": { + "user_config": {} + }, + "dataDirs": { + "root": "/var/lib/astrbot", + "plugin_data": "/var/lib/astrbot/data/plugins/my-plugin", + "temp": "/var/cache/astrbot/temp" + } + } +} +``` + +### Initialize Response(插件 → 主进程) + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "1.0.0", + "serverInfo": { + "name": "weather-plugin", + "version": "1.0.0" + }, + "capabilities": { + "tools": true, + "handlers": true, + "events": true, + "resources": false + }, + "configSchema": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "description": "Weather API Key", + "required": true + }, + "default_location": { + "type": "string", + "description": "默认查询城市", + "default": "北京" + } + } + }, + "metadata": { + "display_name": "天气插件", + "description": "提供天气查询功能", + "author": "Author", + "support_platforms": ["telegram", "discord"] + } + } +} +``` + +### 握手参数说明 + +**主进程 → 插件(Initialize params)**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `pluginConfig.user_config` | object | 用户配置(来自 secrets.yaml 等) | +| `dataDirs.root` | string | AstrBot 数据根目录 | +| `dataDirs.plugin_data` | string | 插件专用数据目录 | +| `dataDirs.temp` | string | 插件临时文件目录 | + +**插件 → 主进程(Initialize result)**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `configSchema` | object | 插件配置 Schema(用于 UI 生成配置表单) | +| `metadata.*` | object | 插件元数据 | + +## 数据目录 + +插件的数据存储目录由主进程分配,插件通过握手获取: + +``` +dataDirs.plugin_data/ +├── config.json # 插件配置(主进程写入) +├── data/ # 插件业务数据 +└── cache/ # 插件缓存 +``` + +**原则**: +- 插件不自行决定数据目录 +- 配置由主进程管理,插件通过 `dataDirs.plugin_data/config.json` 读取 +- 插件只需读写自己的目录,无需了解 AstrBot 目录结构 + +## 配置传递 + +### 1. 用户配置(主进程 → 插件) + +用户通过 WebUI 或配置文件设置的配置,通过握手传递给插件: + +```json +{ + "pluginConfig": { + "user_config": { + "api_key": "xxx", + "default_location": "上海" + } + } +} +``` + +### 2. 配置更新(主进程 → 插件) + +配置变更时,主进程通知插件: + +```json +{ + "jsonrpc": "2.0", + "method": "plugin.config_update", + "params": { + "user_config": { + "api_key": "yyy" + } + } +} +``` + +### 3. 配置 Schema(插件声明) + +插件声明配置 Schema,用于主进程生成配置 UI: + +```json +{ + "configSchema": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "description": "API Key", + "secret": true + }, + "default_location": { + "type": "string", + "description": "默认城市" + } + }, + "required": ["api_key"] + } +} +``` + ## 传输协议 ### Stdio(进程启动) -用于进程内加载或通过 stdio 启动的外部插件: - ``` {"jsonrpc":"2.0","method":"initialize","params":{...},"id":1} {"jsonrpc":"2.0","method":"plugin.handle_event","params":{...}} @@ -96,71 +238,7 @@ Content-Length: <字节数>\r\n ## 消息格式 -所有消息均为 JSON-RPC 2.0: - -### 1. 初始化(initialize) - -**请求**: - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "protocolVersion": "1.0.0", - "clientInfo": { - "name": "astrbot", - "version": "4.16.0" - }, - "capabilities": { - "streaming": true, - "events": true - } - } -} -``` - -**响应**: - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "result": { - "protocolVersion": "1.0.0", - "serverInfo": { - "name": "weather-plugin", - "version": "1.0.0" - }, - "capabilities": { - "tools": true, - "handlers": true, - "events": true - }, - "metadata": { - "display_name": "天气插件", - "description": "提供天气查询功能", - "author": "Author" - } - } -} -``` - -### 2. 插件能力 - -```json -{ - "capabilities": { - "tools": true, // 支持工具调用 - "handlers": true, // 支持消息处理器 - "events": true, // 支持事件订阅 - "resources": false // 是否提供资源 - } -} -``` - -### 3. 工具调用(跨进程) +### 1. 工具调用 **请求**: @@ -186,16 +264,13 @@ Content-Length: <字节数>\r\n "id": "req-001", "result": { "content": [ - { - "type": "text", - "text": "北京天气:晴,25°C" - } + { "type": "text", "text": "北京天气:晴,25°C" } ] } } ``` -### 4. 消息处理 +### 2. 消息处理 **请求**: @@ -210,13 +285,8 @@ Content-Length: <字节数>\r\n "message_id": "msg-123", "unified_msg_origin": "telegram:private:12345", "message_str": "/weather 北京", - "sender": { - "user_id": "12345", - "nickname": "用户" - }, - "message_chain": [ - { "type": "plain", "text": "/weather 北京" } - ] + "sender": { "user_id": "12345", "nickname": "用户" }, + "message_chain": [{ "type": "plain", "text": "/weather 北京" }] } } } @@ -230,29 +300,25 @@ Content-Length: <字节数>\r\n "id": "req-002", "result": { "handled": true, - "results": [ - { "type": "plain", "text": "北京天气:晴,25°C" } - ], + "results": [{ "type": "plain", "text": "北京天气:晴,25°C" }], "stop_propagation": false } } ``` -### 5. 事件订阅 +### 3. 事件订阅 -**通知**(客户端 → 插件): +**通知**(主进程 → 插件): ```json { "jsonrpc": "2.0", "method": "plugin.subscribe", - "params": { - "event_type": "llm_request" - } + "params": { "event_type": "llm_request" } } ``` -**通知**(插件 → 客户端): +**通知**(插件 → 主进程): ```json { @@ -260,80 +326,23 @@ Content-Length: <字节数>\r\n "method": "plugin.notify", "params": { "event_type": "tool_called", - "data": { - "tool": "get_weather", - "args": { "location": "北京" } - } + "data": { "tool": "get_weather", "args": { "location": "北京" } } } } ``` -### 6. 工具列表 - -**请求**: - -```json -{ - "jsonrpc": "2.0", - "id": 2, - "method": "tools/list", - "params": {} -} -``` - -**响应**: - -```json -{ - "jsonrpc": "2.0", - "id": 2, - "result": { - "tools": [ - { - "name": "get_weather", - "description": "获取天气信息", - "inputSchema": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "城市名称" - } - }, - "required": ["location"] - } - } - ] - } -} -``` - -### 7. 资源(可选) - -```json -{ - "jsonrpc": "2.0", - "method": "resources/list", - "params": {} -} -``` - ## 核心方法 -### 初始化 - -| 方法 | 方向 | 描述 | -|------|------|------| -| `initialize` | C→P | 初始化连接 | -| `initialized` | P→C | 初始化完成通知 | - ### 生命周期 | 方法 | 方向 | 描述 | |------|------|------| +| `initialize` | C→P | 初始化连接,交换配置和数据目录 | +| `initialized` | P→C | 初始化完成通知 | | `plugin.start` | C→P | 启动插件 | | `plugin.stop` | C→P | 停止插件 | | `plugin.reload` | C→P | 重载插件 | +| `plugin.config_update` | C→P | 配置更新通知 | ### 工具 @@ -341,7 +350,6 @@ Content-Length: <字节数>\r\n |------|------|------| | `tools/list` | C→P | 列出可用工具 | | `tools/call` | C→P | 调用工具 | -| `tools/metadata` | P→C | 工具元数据更新 | ### 消息处理 @@ -361,7 +369,7 @@ Content-Length: <字节数>\r\n ## 插件元数据 -插件通过 `initialize` 响应返回元数据: +插件在 `initialize` 响应中返回元数据: ```json { @@ -374,13 +382,17 @@ Content-Length: <字节数>\r\n "handlers": true, "events": true }, + "configSchema": { + "type": "object", + "properties": { + "api_key": { "type": "string", "secret": true } + } + }, "metadata": { "display_name": "天气插件", - "description": "提供天气预报功能", - "author": "Author Name", - "homepage": "https://github.com/author/weather-plugin", - "support_platforms": ["telegram", "discord"], - "astrbot_version": ">=4.16,<5" + "description": "提供天气查询功能", + "author": "Author", + "support_platforms": ["telegram", "discord"] } } ``` @@ -409,13 +421,14 @@ Content-Length: <字节数>\r\n ## 进程内插件 -对于进程内插件(load_mode: in_process),编排器直接调用插件方法,无需序列化: +对于进程内插件(load_mode: in_process),主进程直接调用插件方法: ```python -# 插件直接实现为类 class MyPlugin: - def __init__(self, context): + def __init__(self, context, user_config: dict, data_dirs: dict): self.context = context + self.user_config = user_config + self.data_dir = data_dirs["plugin_data"] async def handle_event(self, event): return [PlainResult("Hello!")] @@ -427,19 +440,15 @@ class MyPlugin: return { "name": "my-plugin", "version": "1.0.0", - "capabilities": { - "tools": True, - "handlers": True, - "events": False - } + "capabilities": {"tools": True, "handlers": True, "events": False}, + "configSchema": {...} } ``` -## 实现注意事项 +## 设计原则 -- 插件是独立服务,通过配置声明式加载 -- 支持任意编程语言实现(Python、Go、Rust、JavaScript 等) -- 跨进程插件使用标准 JSON-RPC 2.0 通信 -- 插件元数据通过 `initialize` 响应动态获取 -- 支持热重载:配置更新后可在线重载插件 -- 事件通知采用异步机制,不阻塞主流程 +1. **主进程不读取插件配置**:插件配置通过协议传递 +2. **数据目录由主进程分配**:插件通过握手获取 `dataDirs` +3. **配置 Schema 声明**:插件声明配置结构,主进程生成 UI +4. **插件无侵入性**:插件无需了解 AstrBot 内部结构 +5. **热重载支持**:配置更新通过 `plugin.config_update` 通知 diff --git a/openspec/agent-message.md b/openspec/agent-message.md new file mode 100644 index 000000000..3bf57d1c5 --- /dev/null +++ b/openspec/agent-message.md @@ -0,0 +1,2127 @@ +# Agent 消息处理流程规范 + +## 概述 + +AstrBot Agent 采用**双缓冲区 + 流控**的消息处理模型,实现消息的削峰填谷、限流保护和安全处理。 + +**核心设计**: +- **输入缓冲区**:用户消息暂存,按频率控制消费 +- **输出缓冲区**:回复消息暂存,按策略分发 +- **流控引擎**:根据 API 限制自动调节消费速率 +- **安全层**:防注入、防泄密、防误触 + +## 架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Platform Adapter │ +│ (QQ / Telegram / Discord / ...) │ +└────────────────────────────┬────────────────────────────────────┘ + │ commit_event() + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Input Message Buffer │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ UserQueue (per user/conversation) │ │ +│ │ - metadata: user_id, platform, timestamp, session_id │ │ +│ │ - messages: [msg1, msg2, ...] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ FlowControl │ +│ (rate limiter) │ +└───────────────────────────┼─────────────────────────────────────┘ + │ pull_messages() + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Agent Core │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Context │───▶│ LLM Loop │───▶│ Tool Call │ │ +│ │ Manager │ │ (step loop) │ │ Executor │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└───────────────────────────┬─────────────────────────────────────┘ + │ produce_result() + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Output Buffer │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ResultQueue (per session) │ │ +│ │ - content: string / stream │ │ +│ │ - format: plain / markdown / html │ │ +│ │ - strategy: streaming / segmented / full │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ DispatchStrategy │ +│ (streaming / segmented / full) │ +└───────────────────────────┼─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Platform Adapter │ +│ (SendResult) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 1. 工具、技能与 Agent 协作体系 + +### 1.1 三层架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Agent Core (LLM Loop) │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Internal │ │ MCP │ │ Skills │ │ +│ │ Tools │ │ Tools │ │ │ │ +│ │ (Function │ │ (MCP │ │ (Pre-built │ │ +│ │ Tool) │ │ Client) │ │ Agent │ │ +│ │ │ │ │ │ Flows) │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +│ │ │ │ │ +│ └───────────────────┴───────────────────┘ │ +│ │ │ +│ Tool Executor │ +└──────────────────────────────┼──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Agent 协作层 │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 本地 │ │ 远程 │ │ 子 Agent │ │ +│ │ Subagent │ │ A2A Agent │ │ (MCP/A2A) │ │ +│ │ │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ ACP 协议 (Agent 通信) │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.2 工具来源 + +| 来源 | 协议 | 说明 | +|------|------|------| +| **Internal Tools** | 自定义 Python | `FunctionTool`/`ToolSet`,Star 插件注册 | +| **MCP Tools** | MCP JSON-RPC 2.0 | 外部 MCP 服务器提供的工具 | +| **Skills** | 自定义协议 | 预构建的 Agent 执行流程模板 | + +### 1.3 工具调用决策 + +```python +class ToolRouter: + """工具路由""" + + def __init__( + self, + internal_toolset: ToolSet, + mcp_clients: dict[str, MCPClient], + skill_executors: dict[str, SkillExecutor], + ): + self.internal = internal_toolset + self.mcp = mcp_clients + self.skills = skill_executors + + async def route_tool_call( + self, + tool_name: str, + arguments: dict, + context: AgentContext, + ) -> ToolResult: + """路由工具调用""" + + # 1. 检查内部工具 + internal_tool = self.internal.get_tool(tool_name) + if internal_tool: + return await self._call_internal(internal_tool, arguments, context) + + # 2. 检查 MCP 工具 + for client_name, client in self.mcp.items(): + if client.has_tool(tool_name): + return await client.call_tool(tool_name, arguments) + + # 3. 检查 Skills + skill = self.skills.get(tool_name) + if skill: + return await self._execute_skill(skill, arguments, context) + + raise ToolNotFoundError(f"Tool not found: {tool_name}") +``` + +### 1.4 Agent 协作(ACP 协议) + +```python +class ACPAgentClient: + """ACP Agent 客户端""" + + async def call_agent( + self, + agent_name: str, + action: str, + args: dict, + stream: bool = True, + ) -> AsyncIterator[AgentEvent] | AgentResult: + """调用远程 Agent""" + + request = ACPRequest( + method="agent/call", + params={ + "agent": agent_name, + "action": action, + "args": args, + } + ) + + if stream: + return self._stream_request(request) + else: + return await self._send_request(request) + + async def list_agents(self) -> list[AgentCard]: + """列出可用 Agent""" + response = await self._send_request( + ACPRequest(method="agent/list") + ) + return [AgentCard(**a) for a in response.result["agents"]] +``` + +### 1.5 Skills 执行 + +```python +class SkillExecutor: + """Skill 执行器""" + + def __init__(self, skill_registry: SkillRegistry): + self.registry = skill_registry + + async def execute( + self, + skill_name: str, + input_data: dict, + context: AgentContext, + ) -> SkillResult: + """执行 Skill""" + + skill = self.registry.get(skill_name) + if not skill: + raise SkillNotFoundError(f"Skill not found: {skill_name}") + + # Skill 可以包含多个步骤 + steps = skill.get_steps() + + results = [] + for step in steps: + # 每个步骤可以是工具调用或 Agent 调用 + if step.type == "tool": + result = await self._call_tool(step.tool, step.args) + elif step.type == "agent": + result = await self._call_agent(step.agent, step.action, step.args) + elif step.type == "llm": + result = await self._call_llm(step.prompt, context) + + results.append(result) + + # 检查是否需要停止 + if step.on_result == "stop_if_success" and result.success: + break + + return SkillResult( + skill_name=skill_name, + steps=results, + final_output=results[-1] if results else None, + ) +``` + +### 1.6 配置 + +```yaml +# agent.yaml + +# 工具配置 +tools: + # 内部工具 + internal: + enabled: true + max_per_request: 128 + + # MCP 工具 + mcp: + enabled: true + servers: [] # MCP 服务器配置 + + # Skills + skills: + enabled: true + registry_path: "$XDG_DATA_HOME/astrbot/skills/" + +# Agent 协作配置 +agent_collaboration: + # ACP 配置 + acp: + enabled: true + endpoints: + - name: "local" + type: "unix" + path: "/run/astrbot/acp.sock" + + # 子 Agent 配置 + subagents: + enabled: true + max_parallel: 3 + timeout: 300 + + # Agent 发现 + discovery: + # 自动发现同进程内的 Subagent + auto_discover_internal: true + + # 定期刷新远程 Agent 列表 + refresh_interval: 60 +``` + +--- + +## 2. 输入缓冲区(Input Buffer) + +### 2.1 队列结构 + +```python +@dataclass +class InputMessage: + """输入消息单元""" + message_id: str # 全局唯一 ID + platform: str # 平台标识 + user_id: str # 用户 ID + conversation_id: str # 会话 ID + content: MessageChain | str # 消息内容 + timestamp: float # 到达时间 + metadata: dict # 扩展元数据 + priority: int = 0 # 优先级(越高越先处理) + +@dataclass +class UserMessageQueue: + """用户消息队列""" + user_id: str + session_id: str + messages: deque[InputMessage] # 消息列表(有序) + metadata: dict # 用户元数据 + created_at: float + updated_at: float + max_size: int = 1000 # 最大消息数 + max_age: float = 3600 # 消息最大存活时间(秒) +``` + +### 2.2 缓冲区配置 + +```yaml +# agent.yaml +input_buffer: + # 单用户队列最大消息数 + max_queue_size: 1000 + + # 消息最大存活时间(秒) + max_message_age: 3600 + + # 超出限制时的处理策略 + overflow_strategy: "drop_oldest" # drop_oldest | drop_newest | block + + # 丢弃消息时的提示前缀 + overflow_hint: "[消息过多,部分早期消息已丢弃]" + + # 是否按用户隔离队列 + per_user_queue: true + + # 是否按会话隔离队列 + per_conversation_queue: true +``` + +### 2.3 溢出保护策略 + +| 策略 | 说明 | 适用场景 | +|------|------|----------| +| `drop_oldest` | 丢弃最旧的消息,保留最新的 | 高频聊天,侧重时效性 | +| `drop_newest` | 丢弃最新的消息,保留旧的 | 重要指令,不容丢失 | +| `block` | 阻塞输入,直到队列有空位 | 重要对话,不容任何丢弃 | + +**溢出时的处理**: + +```python +async def add_message(queue: UserMessageQueue, message: InputMessage) -> None: + if len(queue.messages) >= queue.max_size: + if queue.overflow_strategy == "drop_oldest": + old_msg = queue.messages.popleft() + # 在丢弃的消息前插入提示 + hint = InputMessage( + content=f"[{queue.overflow_hint} 丢弃于 {old_msg.timestamp}]", + message_id="system_hint", + # ... + ) + queue.messages.appendleft(hint) + elif queue.overflow_strategy == "drop_newest": + return # 丢弃新消息 + elif queue.overflow_strategy == "block": + await queue.not_full.wait() # 阻塞等待 +``` + +--- + +## 3. 流控引擎(Flow Control) + +### 3.1 速率限制配置 + +```yaml +# agent.yaml +flow_control: + # 消费速率模式 + mode: "auto" # auto | manual + + # 手动模式:每秒处理消息数 + manual_rate: 10 + + # 自动模式:基于 LLM API 限制计算 + auto: + # LLM API 每分钟请求限制 + api_rpm_limit: 60 + + # 每次请求预计处理消息数 + messages_per_request: 5 + + # 安全系数(留一定余量) + safety_margin: 0.8 + + # 最小消费间隔(秒) + min_interval: 0.5 + + # 最大消费间隔(秒) + max_interval: 10 +``` + +### 3.2 速率计算公式 + +``` +effective_rate = min(api_rpm_limit * messages_per_request * safety_margin, 1/min_interval) +consume_interval = 1 / effective_rate +``` + +**示例**: +- API RPM = 60 +- 每请求处理 5 条消息 +- 安全系数 = 0.8 +- 有效速率 = 60 * 5 * 0.8 = 240 消息/分钟 = 4 消息/秒 +- 消费间隔 = 0.25 秒 + +### 3.3 令牌桶实现 + +```python +class TokenBucket: + """令牌桶流控""" + + def __init__( + self, + rate: float, # 每秒令牌数 + capacity: float, # 桶容量 + burst: float = None, # 突发容量 + ): + self.rate = rate + self.capacity = capacity + self.burst = burst or capacity + self.tokens = capacity + self.last_update = time.monotonic() + + async def acquire(self, tokens: float = 1.0) -> float: + """获取令牌,返回需要等待的秒数""" + now = time.monotonic() + elapsed = now - self.last_update + self.tokens = min(self.capacity, self.tokens + elapsed * self.rate) + self.last_update = now + + if self.tokens >= tokens: + self.tokens -= tokens + return 0.0 + + wait_time = (tokens - self.tokens) / self.rate + return wait_time + + async def wait_and_acquire(self, tokens: float = 1.0) -> None: + """等待直到获取令牌""" + wait = await self.acquire(tokens) + if wait > 0: + await asyncio.sleep(wait) +``` + +### 3.4 优先级调度 + +```python +class PriorityScheduler: + """优先级调度器""" + + def __init__(self, buckets: dict[str, TokenBucket]): + self.buckets = buckets # per-user or per-session + + async def next_message(self) -> InputMessage | None: + """获取下一条待处理消息(按优先级)""" + # 1. 收集所有非空队列 + candidates = [] + for user_id, queue in self.queues.items(): + if not queue.messages: + continue + + # 2. 计算该用户的可用速率 + bucket = self.buckets.get(user_id) + if not bucket: + continue + + # 3. 获取队首消息(peek,不移除) + msg = queue.messages[0] + + candidates.append((msg, bucket, user_id)) + + if not candidates: + return None + + # 4. 按优先级 + 可用性排序 + # 优先级相同时,优先处理令牌充足的 + candidates.sort( + key=lambda x: ( + -x[0].priority, + x[1].tokens / x[1].rate if x[1].rate > 0 else 0 + ) + ) + + # 5. 等待最紧急消息的令牌 + msg, bucket, user_id = candidates[0] + await bucket.wait_and_acquire(1.0) + + # 6. 移除并返回 + return queue.messages.popleft() +``` + +--- + +## 4. Agent 核心(Agent Core) + +### 4.1 上下文管理(Context Manager) + +```python +@dataclass +class AgentContext: + """Agent 执行上下文""" + messages: list[Message] # 消息历史 + system_prompt: str # 系统提示 + tools: list[ToolDefinition] # 可用工具 + memory: MemoryBank # 记忆存储 + metadata: dict # 扩展元数据 + +class ContextManager: + """上下文管理器""" + + def __init__(self, config: ContextConfig): + self.max_tokens: int = config.max_context_tokens + self.compress_threshold: float = config.compress_threshold + self.keep_recent: int = config.keep_recent_messages + + def build_context( + self, + queue: UserMessageQueue, + memory: MemoryBank, + ) -> AgentContext: + """构建 Agent 执行上下文""" + + # 1. 从队列获取消息 + raw_messages = list(queue.messages) + + # 2. 应用安全过滤 + raw_messages = self.apply_security_filters(raw_messages) + + # 3. 构建消息列表 + messages = self.build_message_list(raw_messages) + + # 4. 检查是否需要压缩 + total_tokens = self.estimate_tokens(messages) + + if total_tokens > self.max_tokens * self.compress_threshold: + messages = self.compress_context(messages, memory) + + # 5. 添加系统提示 + system_prompt = self.build_system_prompt() + + return AgentContext( + messages=messages, + system_prompt=system_prompt, + tools=self.get_available_tools(), + memory=memory, + ) + + def compress_context( + self, + messages: list[Message], + memory: MemoryBank, + ) -> list[Message]: + """压缩上下文""" + + # 保留最近 N 条消息 + recent = messages[-self.keep_recent:] + + # 提取历史消息进行压缩 + history = messages[:-self.keep_recent] + + # 摘要历史消息并存入记忆 + if history: + summary = self.summarize(history) + memory.add(Message( + role="system", + content=f"[历史摘要] {summary}", + metadata={"type": "summary"} + )) + + return recent +``` + +### 4.2 上下文配置 + +```yaml +# agent.yaml +context: + # 最大上下文 token 数 + max_context_tokens: 128000 + + # 触发压缩的阈值(比例) + compress_threshold: 0.85 + + # 压缩后保留的最近消息数 + keep_recent_messages: 6 + + # 压缩提供者(为空则使用主 Provider) + compress_provider_id: "" + + # 压缩提示词 + compress_instruction: | + 请简洁地总结对话要点,保留关键信息如: + - 用户的主要需求或问题 + - 已确定的方案或结论 + - 未完成的任务 + + # 消息保留策略 + retention: + # 保留最近 N 小时内的原始消息 + recent_hours: 24 + + # 超出后转为摘要存储 + summarize_after: true +``` + +--- + +## 5. 工具调用策略(Tool Calling Strategy) + +### 4.1 工具调用最佳实践 + +```yaml +# agent.yaml +tool_calling: + # 工具调用策略 + strategy: "smart" # eager | sequential | smart + + # 每次请求最大工具调用数 + max_calls_per_request: 128 + + # 工具调用超时(秒) + timeout: 60 + + # 工具调用失败重试次数 + max_retries: 3 + + # 是否并行调用独立工具 + parallel_calls: true + + # 并行调用最大数量 + max_parallel_calls: 5 + + # 工具结果的最大 token 数(截断) + max_result_tokens: 4096 + + # 是否在工具调用后立即返回中间结果 + stream_intermediate: true +``` + +### 4.2 工具调用流程 + +```python +class ToolCallingPolicy: + """工具调用策略""" + + async def execute_tools( + self, + llm_response: LLMResponse, + context: AgentContext, + ) -> list[ToolResult]: + """执行工具调用""" + + # 1. 解析工具调用请求 + tool_calls = llm_response.tool_calls or [] + + if not tool_calls: + return [] + + # 2. 按策略分组 + groups = self._group_by_dependency(tool_calls) + + results = [] + + # 3. 按组执行 + for group in groups: + if self._can_parallel(group): + # 并行执行 + group_results = await asyncio.gather( + *[self._execute_single(call, context) for call in group], + return_exceptions=True + ) + else: + # 串行执行 + group_results = [] + for call in group: + result = await self._execute_single(call, context) + group_results.append(result) + + results.extend(group_results) + + # 4. 检查是否超过限制 + if len(results) >= self.config.max_calls_per_request: + break + + # 5. 如果需要流式中间结果 + if self.config.stream_intermediate: + yield ToolCallingEvent( + type="intermediate", + results=group_results + ) + + return results + + def _group_by_dependency( + self, + tool_calls: list[ToolCall], + ) -> list[list[ToolCall]]: + """按依赖关系分组""" + + groups = [] + current_group = [] + + for call in tool_calls: + # 检查是否依赖前一个工具的结果 + if call.arguments_depends_on_previous and current_group: + # 依赖:将当前调用加入前一个组 + current_group.append(call) + else: + # 不依赖:开启新组 + if current_group: + groups.append(current_group) + current_group = [call] + + if current_group: + groups.append(current_group) + + return groups +``` + +### 4.3 工具选择策略 + +```python +class ToolSelector: + """工具选择策略""" + + def __init__(self, config: ToolSelectionConfig): + self.max_tools_per_request = config.max_tools_per_request + self.prefer_recent = config.prefer_recent_tools + + def select_tools( + self, + available_tools: list[Tool], + query: str, + context: AgentContext, + ) -> list[Tool]: + """选择最相关的工具""" + + # 1. 计算工具与查询的相关性 + scored = [] + for tool in available_tools: + score = self._calculate_relevance(tool, query, context) + scored.append((score, tool)) + + # 2. 排序并截取 + scored.sort(key=lambda x: -x[0]) + selected = scored[:self.max_tools_per_request] + + # 3. 如果启用了最近使用优先 + if self.prefer_recent: + selected = self._boost_recent(selected, context) + + return [t for _, t in selected] + + def _calculate_relevance( + self, + tool: Tool, + query: str, + context: AgentContext, + ) -> float: + """计算相关性分数""" + + base_score = 0.0 + + # 工具名称匹配 + if any(word in tool.name.lower() for word in query.lower().split()): + base_score += 0.3 + + # 工具描述匹配 + if tool.description: + # 简单的词重叠计算 + query_words = set(query.lower().split()) + desc_words = set(tool.description.lower().split()) + overlap = len(query_words & desc_words) + base_score += overlap * 0.1 + + # 最近使用过的工具加权 + if context.metadata.get("recent_tools"): + if tool.name in context.metadata["recent_tools"]: + base_score += 0.2 + + return base_score +``` + +--- + +## 6. 安全层(Security Layer) + +### 6.1 安全配置 + +```yaml +# agent.yaml +security: + # 防注入配置 + injection: + # 启用防注入 + enable: true + + # 检测模式 + mode: "strict" # strict | moderate | permissive + + # 注入模式识别 + patterns: + - name: "role_play_injection" + regex: "(?i)(you are now|forget previous|ignore all)" + severity: "high" + + - name: "system_prompt_leak" + regex: "(?i)(repeat your? (system|initial) (prompt|instructions))" + severity: "high" + + - name: "code_injection" + regex: "(?i)(```(system|prompt|instructor))" + severity: "medium" + + # 触发时的处理策略 + on_detect: "sanitize" # sanitize | block | warn + + # 是否记录检测日志 + log_detections: true + + # 内容过滤配置 + content_filter: + # 启用内容过滤 + enable: true + + # 过滤级别 + level: "standard" # strict | standard | minimal + + # 敏感词列表(文件路径或内联) + blocklist: [] + + # 替换字符 + replacement: "[已过滤]" + + # 泄密防护 + leakage_prevention: + # 阻止 Agent 读取敏感文件模式 + blocked_file_patterns: + - "**/.env" + - "**/secrets.yaml" + - "**/*password*" + - "**/.git/credentials" + + # 阻止 Agent 输出敏感信息模式 + blocked_output_patterns: + - "(?i)api[_-]?key" + - "(?i)secret" + - "(?i)password" + + # 替换为占位符 + placeholder: "[REDACTED]" +``` + +### 6.2 安全过滤器实现 + +```python +class SecurityFilter: + """安全过滤器""" + + def __init__(self, config: SecurityConfig): + self.config = config + self.compiled_patterns = [ + (p["name"], re.compile(p["regex"]), p["severity"]) + for p in config.injection.patterns + ] + + def filter_messages( + self, + messages: list[InputMessage], + ) -> list[InputMessage]: + """过滤输入消息""" + + filtered = [] + + for msg in messages: + # 1. 内容过滤 + if self.config.content_filter.enable: + msg.content = self._filter_content(msg.content) + + # 2. 注入检测 + if self.config.injection.enable: + detections = self._detect_injection(msg.content) + + if detections: + action = self._handle_injection(detections, msg) + if action == "skip": + continue + + filtered.append(msg) + + return filtered + + def filter_output( + self, + content: str, + context: AgentContext, + ) -> str: + """过滤输出内容""" + + # 1. 泄密防护 - 移除敏感信息 + if self.config.leakage_prevention: + content = self._redact_sensitive(content) + + return content + + def _detect_injection(self, content: str) -> list[Detection]: + """检测注入攻击""" + detections = [] + + for name, pattern, severity in self.compiled_patterns: + if pattern.search(content): + detections.append(Detection( + name=name, + severity=severity, + matched=pattern.findall(content), + )) + + return detections + + def _handle_injection( + self, + detections: list[Detection], + message: InputMessage, + ) -> str: + """处理注入检测""" + + high_severity = any(d.severity == "high" for d in detections) + + if high_severity and self.config.injection.on_detect == "block": + # 记录并阻止 + logging.warning(f"Blocked injection: {detections}") + return "skip" + + elif self.config.injection.on_detect == "sanitize": + # 消毒处理 + for detection in detections: + message.content = message.content.replace( + detection.matched, + self.config.content_filter.replacement, + ) + return "sanitize" + + return "allow" + + def _filter_content(self, content: str) -> str: + """内容过滤""" + + if not self.config.content_filter.enable: + return content + + for pattern in self.config.content_filter.blocklist: + content = re.sub(pattern, self.config.content_filter.replacement, content) + + return content +``` + +--- + +## 7. 权限模型(Permission Model) + +### 7.1 设计原则 + +遵循 **Unix 哲学**,权限模型采用类似 `rwx` 的能力(Capability)设计: + +| 原则 | 说明 | +|------|------| +| **最小权限** | 只授予完成任务所需的最小权限集 | +| **能力继承** | 高权限自动包含低权限的能力 | +| **可组合** | 权限可以灵活组合,适应不同场景 | +| **可委托** | 支持权限的委托和回收 | + +### 7.2 角色定义 + +```rust +/// 角色枚举,类比 Unix 用户组 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Role { + Owner = 0o700, // 超级管理员/拥有者 + Admin = 0o600, // 普通管理员 + Member = 0o400, // 普通成员 + Guest = 0o100, // 访客(受限) + Blocked = 0o000, // 被封禁 +} + +bitflags::bitflags! { + /// 权限枚举,类比 rwx + pub struct Permission: u16 { + // 基础权限 + const READ = 0o400; // 读取权限 + const WRITE = 0o200; // 写入权限 + const EXECUTE = 0o100; // 执行权限 + + // 消息权限 + const SEND_MESSAGE = 0o040; // 发送消息 + const SEND_MEDIA = 0o020; // 发送媒体 + const SEND_COMMAND = 0o010; // 发送命令 + + // 管理权限 + const MANAGE_MEMBER = 0o004; // 管理成员 + const MANAGE_CONFIG = 0o002; // 管理配置 + const MANAGE_PERMISSION = 0o001; // 管理权限 + + // 特殊权限 + const BOT_ADMIN = 0o700; // Bot 管理员(全权限) + const OWNER_ONLY = 0o100; // 仅拥有者可用 + } +} + +impl Role { + /// 检查角色是否拥有指定权限 + pub fn has_permission(&self, permission: Permission) -> bool { + let role_bits = self.bits(); + (role_bits & permission.bits()) == permission.bits() + } + + /// 获取角色的权限位 + fn bits(&self) -> u16 { + *self as u16 + } +} +``` + +### 7.3 能力矩阵 + +``` +┌──────────────────┬───────┬───────┬────────┬────────┬──────────┐ +│ 能力 │ OWNER │ ADMIN │ MEMBER │ GUEST │ BLOCKED │ +├──────────────────┼───────┼───────┼────────┼────────┼──────────┤ +│ 读取消息 │ ✓ │ ✓ │ ✓ │ ✓ │ ✗ │ +│ 发送普通消息 │ ✓ │ ✓ │ ✓ │ ✓ │ ✗ │ +│ 发送媒体 │ ✓ │ ✓ │ ✓ │ ✗ │ ✗ │ +│ 发送斜杠命令 │ ✓ │ ✓ │ ✓ │ ✗ │ ✗ │ +│ 使用管理员命令 │ ✓ │ ✓ │ ✗ │ ✗ │ ✗ │ +│ 管理成员 │ ✓ │ ✓ │ ✗ │ ✗ │ ✗ │ +│ 修改配置 │ ✓ │ ✗ │ ✗ │ ✗ │ ✗ │ +│ 转让所有权 │ ✓ │ ✗ │ ✗ │ ✗ │ ✗ │ +│ 踢出 Bot │ ✓ │ ✗ │ ✗ │ ✗ │ ✗ │ +└──────────────────┴───────┴───────┴────────┴────────┴──────────┘ +``` + +### 7.4 权限检查流程 + +```rust +use async_trait::async_trait; + +#[async_trait] +pub trait PermissionCheck { + async fn check_message( + &self, + event: &InputMessage, + context: &AgentContext, + ) -> PermissionResult; +} + +pub struct PermissionMiddleware { + role_config: RoleConfig, + command_permissions: HashMap, +} + +#[derive(Debug)] +pub struct PermissionResult { + pub allowed: bool, + pub reason: Option, +} + +impl PermissionMiddleware { + /// 检查消息权限 + async fn check_message( + &self, + event: &InputMessage, + context: &AgentContext, + ) -> PermissionResult { + // 1. 获取发送者角色 + let role = self + .get_user_role(&event.user_id, &event.conversation_id) + .await; + + // 2. 检查基础消息权限 + if !role.has_permission(Permission::SEND_MESSAGE) { + return PermissionResult { + allowed: false, + reason: Some("用户被禁止发送消息".into()), + }; + } + + // 3. 检查媒体权限 + if event.has_media && !role.has_permission(Permission::SEND_MEDIA) { + return PermissionResult { + allowed: false, + reason: Some("用户被禁止发送媒体".into()), + }; + } + + // 4. 检查命令权限 + if event.is_command { + let cmd_perm = self + .command_permissions + .get(&event.command_name) + .copied() + .unwrap_or(Permission::EXECUTE); + + if !role.has_permission(cmd_perm) { + return PermissionResult { + allowed: false, + reason: Some(format!("用户无权执行命令: {}", event.command_name)), + }; + } + } + + PermissionResult { allowed: true, reason: None } + } +} +``` + +### 7.5 命令权限配置 + +```yaml +# agent.yaml +permissions: + # 默认角色权限 + default_role: "member" + + # 角色能力定义 + roles: + owner: + capabilities: 0o700 + inherits: ["admin"] + + admin: + capabilities: 0o600 + inherits: ["member"] + + member: + capabilities: 0o400 + inherits: ["guest"] + + guest: + capabilities: 0o100 + inherits: [] + + blocked: + capabilities: 0o000 + inherits: [] + + # 斜杠命令权限 + commands: + # 公开命令(所有人均可使用) + public: + - "/help" + - "/status" + - "/ping" + + # 成员命令(member 及以上) + member: + - "/search" + - "/weather" + - "/translate" + + # 管理员命令(admin 及以上) + admin: + - "/kick" + - "/ban" + - "/mute" + - "/warn" + - "/config" + + # 拥有者命令(仅 owner) + owner: + - "/transfer" + - "/delete" + - "/backup" + - "/reload" + + # 权限继承配置 + inheritance: + enabled: true + max_depth: 5 # 最大继承深度,防止循环 +``` + +### 7.6 用户角色管理 + +```rust +#[async_trait] +pub trait RoleManager: Send + Sync { + /// 获取用户在特定会话中的角色 + async fn get_role( + &self, + user_id: &str, + conversation_id: &str, + ) -> Role; + + /// 设置用户角色(需要相应权限) + async fn set_role( + &self, + user_id: &str, + conversation_id: &str, + role: Role, + operator_id: &str, + ) -> Result<(), PermissionDenied>; + + /// 转让所有权 + async fn transfer_ownership( + &self, + conversation_id: &str, + new_owner_id: &str, + ) -> Result<(), PermissionDenied>; +} + +pub struct SqliteRoleManager { + pool: SqlitePool, +} + +#[derive(Debug, thiserror::Error)] +pub enum PermissionDenied { + #[error("权限不足: {0}")] + Insufficient(String), + #[error("无法设置比自己更高的权限")] + CannotElevate, +} + +#[async_trait] +impl RoleManager for SqliteRoleManager { + async fn get_role( + &self, + user_id: &str, + conversation_id: &str, + ) -> Role { + // 1. 检查全局管理员 + if self.is_global_admin(user_id).await { + return Role::Owner; + } + + // 2. 检查会话特定角色 + if let Some(role_data) = self.storage.get_user_role(user_id, conversation_id).await { + return Role::from_bits(role_data.role); + } + + // 3. 返回默认角色 + Role::Member + } + + async fn set_role( + &self, + user_id: &str, + conversation_id: &str, + role: Role, + operator_id: &str, + ) -> Result<(), PermissionDenied> { + let operator_role = self.get_role(operator_id, conversation_id).await; + + // 检查操作者权限 + if role.bits() > operator_role.bits() { + return Err(PermissionDenied::CannotElevate); + } + + self.storage + .set_user_role(user_id, conversation_id, role.bits()) + .await; + + Ok(()) + } +} +``` + +### 7.7 会话级权限配置 + +```rust +#[derive(Debug, Clone)] +pub struct ConversationPermissions { + pub conversation_id: String, + + // 基础权限 + pub default_role: Role, + pub allow_guest_read: bool, + pub allow_guest_send: bool, + + // 功能开关 + pub allow_media: bool, + pub allow_commands: bool, + pub allow_ai_responses: bool, + + // 限制 + pub max_message_length: usize, + pub max_messages_per_minute: usize, + pub max_commands_per_minute: usize, + + // 白名单/黑名单 + pub whitelist: Vec, + pub blacklist: Vec, +} + +impl ConversationPermissions { + /// 检查用户是否允许执行操作 + pub fn check_user_allowed(&self, user_id: &str, permission: Permission) -> bool { + if self.blacklist.contains(&user_id.to_string()) { + return false; + } + + if !self.whitelist.is_empty() && !self.whitelist.contains(&user_id.to_string()) { + return false; + } + + true + } +} +``` + +### 7.8 权限事件 + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PermissionEvent { + RoleChanged, + PermissionDenied, + UserBanned, + UserUnbanned, + CommandBlocked, + OwnershipTransferred, +} + +#[derive(Debug, Clone)] +pub struct PermissionAuditLog { + pub event: PermissionEvent, + pub operator_id: String, + pub target_id: String, + pub conversation_id: String, + pub details: HashMap, + pub timestamp: i64, +} +``` + +### 7.9 与 Unix 的类比 + +``` +┌─────────────────┬────────────────────────┐ +│ Unix 概念 │ AstrBot 对应 │ +├─────────────────┼────────────────────────┤ +│ 用户 (User) │ 用户 (User) │ +│ 用户组 (Group) │ 会话 (Conversation) │ +│ root 用户 │ Owner (拥有者) │ +│ sudo 用户 │ Admin (管理员) │ +│ 普通用户 │ Member (成员) │ +│ 访客 │ Guest (访客) │ +│ 文件权限 rwx │ 能力 (Capability) │ +│ chmod │ set_role │ +│ chown │ transfer_ownership │ +│ /etc/passwd │ Role Storage │ +└─────────────────┴────────────────────────┘ +``` + +--- + +## 8. 输出缓冲区(Output Buffer) + +### 8.1 队列结构 + +```python +@dataclass +class OutputMessage: + """输出消息单元""" + session_id: str + content: str | AsyncIterator[str] # 支持流式 + format: str = "plain" # plain | markdown | html + strategy: OutputStrategy = OutputStrategy.FULL + metadata: dict = field(default_factory=dict) + + # 流式相关 + stream_start_time: float | None = None + total_sent: int = 0 + +@dataclass +class ResultQueue: + """结果队列""" + session_id: str + results: deque[OutputMessage] + max_size: int = 100 + allow_streaming: bool = True + +class OutputStrategy(Enum): + """输出策略""" + STREAMING = "streaming" # 流式输出 + SEGMENTED = "segmented" # 智能分段 + FULL = "full" # 一次性输出 +``` + +### 8.2 输出策略 + +```yaml +# agent.yaml +output: + # 默认输出策略 + default_strategy: "streaming" # streaming | segmented | full + + # 流式配置 + streaming: + # 启用流式 + enable: true + + # 流式 Chunk 大小(字符数) + chunk_size: 20 + + # Chunk 之间的间隔(秒) + chunk_interval: 0.05 + + # 智能分段配置 + segmented: + # 启用智能分段 + enable: true + + # 触发分段的字数阈值 + threshold: 500 + + # 分段方式 + mode: "sentence" # sentence | word_count | regex + + # 按句子分段时的最小长度 + min_segment_length: 50 + + # 分段正则(当 mode=regex) + split_regex: "[。!?;\n]+" + + # 段落之间的随机间隔(秒) + random_interval: "0.5,2.0" + + # 是否在分段前添加省略号 + add_ellipsis: true + + # 平台适配 + platform_adaptation: + # 平台与策略映射 + strategy_by_platform: + telegram: "segmented" # Telegram 有字数限制 + discord: "segmented" # Discord 也有限制 + qq: "segmented" + webchat: "streaming" # WebChat 支持流式 + + # 平台消息长度限制 + max_length_by_platform: + telegram: 4096 + discord: 2000 + qq: 500 + + # 输出缓冲配置 + buffer: + # 最大缓冲消息数 + max_size: 100 + + # 消息最大存活时间(秒) + max_age: 300 + + # 溢出策略 + overflow_strategy: "drop_oldest" +``` + +### 8.3 分段器实现 + +```python +class SmartSegmenter: + """智能分段器""" + + def __init__(self, config: SegmentedConfig): + self.config = config + + def segment(self, content: str) -> list[str]: + """将内容分段""" + + if len(content) < self.config.threshold: + return [content] + + if self.config.mode == "sentence": + return self._split_by_sentence(content) + elif self.config.mode == "word_count": + return self._split_by_word_count(content) + elif self.config.mode == "regex": + return self._split_by_regex(content) + + return [content] + + def _split_by_sentence(self, content: str) -> list[str]: + """按句子分段""" + sentences = re.split( + self.config.split_regex, + content, + ) + + segments = [] + current = [] + + for sentence in sentences: + if not sentence.strip(): + continue + + current.append(sentence) + current_text = "".join(current) + + # 如果当前段落达到阈值 + if len(current_text) >= self.config.threshold: + segment = "".join(current) + if self.config.add_ellipsis and len(segments) > 0: + segment = "..." + segment.lstrip() + segments.append(segment) + current = [] + + # 处理剩余内容 + if current: + remaining = "".join(current) + if remaining.strip(): + if self.config.add_ellipsis and segments: + remaining = "..." + remaining.lstrip() + segments.append(remaining) + + return segments + + async def stream_segments( + self, + content: str, + output: OutputMessage, + sender: Callable[[str], Awaitable[None]], + ) -> None: + """流式发送分段""" + + segments = self.segment(content) + + for i, segment in enumerate(segments): + # 发送当前分段 + await sender(segment) + + # 添加间隔(随机) + if i < len(segments) - 1: + interval = self._random_interval() + await asyncio.sleep(interval) + + def _random_interval(self) -> float: + """生成随机间隔""" + import random + parts = self.config.random_interval.split(",") + return random.uniform(float(parts[0]), float(parts[1])) +``` + +### 8.4 流式输出器 + +```python +class StreamingOutput: + """流式输出器""" + + def __init__(self, config: StreamingConfig): + self.config = config + + async def stream( + self, + content: str, + sender: Callable[[str], Awaitable[None]], + ) -> None: + """流式输出内容""" + + start = 0 + while start < len(content): + end = start + self.config.chunk_size + chunk = content[start:end] + + await sender(chunk) + + start = end + + # 添加短暂间隔 + if start < len(content): + await asyncio.sleep(self.config.chunk_interval) + + def create_stream( + self, + content: str, + ) -> AsyncIterator[str]: + """创建流式迭代器""" + + async def generator(): + start = 0 + while start < len(content): + end = start + self.config.chunk_size + chunk = content[start:end] + yield chunk + start = end + + if start < len(content): + await asyncio.sleep(self.config.chunk_interval) + + return generator() +``` + +--- + +## 9. 记忆管理(Memory Management) + +### 9.1 记忆存储配置 + +```yaml +# agent.yaml +memory: + # 记忆存储类型 + backend: "sqlite" # sqlite | redis | memory + + # SQLite 配置 + sqlite: + path: "$XDG_DATA_HOME/astrbot/state/memory.db" + + # Redis 配置 + redis: + host: "localhost" + port: 6379 + db: 0 + prefix: "astrbot:memory:" + + # 记忆保留策略 + retention: + # 工作记忆:保留在数据库中的时间(天) + working_memory_days: 7 + + # 长期记忆:超过后转为归档 + long_term_threshold_days: 30 + + # 自动摘要阈值(对话轮数) + auto_summary_threshold: 50 + + # 每次摘要保留的关键信息数 + summary_keep_key_points: 5 + + # 上下文窗口内的记忆 + context_window: + # 保留最近 N 轮对话的完整记忆 + recent_rounds: 10 + + # 超出后转为摘要 + summarize_beyond: true +``` + +### 9.2 记忆类型 + +```python +class MemoryType(Enum): + """记忆类型""" + WORKING = "working" # 工作记忆(当前会话) + EPISODIC = "episodic" # 情景记忆(历史事件) + SEMANTIC = "semantic" # 语义记忆(持久知识) + +@dataclass +class MemoryEntry: + """记忆条目""" + id: str + type: MemoryType + content: str + embedding: list[float] | None = None + metadata: dict = field(default_factory=dict) + created_at: float + updated_at: float + access_count: int = 0 + importance: float = 0.5 # 0-1 重要性评分 + +class MemoryBank: + """记忆库""" + + def __init__(self, config: MemoryConfig): + self.config = config + self.backend = self._create_backend(config) + self._cache: dict[str, MemoryEntry] = {} + self._cache_max_size = 100 + + async def add(self, message: Message) -> None: + """添加记忆""" + entry = MemoryEntry( + id=str(uuid.uuid4()), + type=MemoryType.EPISODIC, + content=message.content, + metadata={ + "role": message.role, + "user_id": message.metadata.get("user_id"), + "session_id": message.metadata.get("session_id"), + }, + created_at=time.time(), + updated_at=time.time(), + ) + + await self.backend.save(entry) + + async def search( + self, + query: str, + limit: int = 5, + memory_types: list[MemoryType] | None = None, + ) -> list[MemoryEntry]: + """搜索记忆""" + + # 1. 如果有缓存,直接返回 + cache_key = f"{query}:{limit}" + if cache_key in self._cache: + return self._cache[cache_key] + + # 2. 向量搜索 + results = await self.backend.search( + query=query, + limit=limit, + memory_types=memory_types, + ) + + # 3. 更新访问计数 + for entry in results: + entry.access_count += 1 + await self.backend.update(entry) + + # 4. 缓存 + if len(self._cache) >= self._cache_max_size: + # LRU 淘汰 + oldest = min(self._cache.values(), key=lambda x: x.access_count) + del self._cache[oldest.id] + + self._cache[cache_key] = results + + return results + + async def summarize_old( + self, + before_timestamp: float, + ) -> str: + """摘要旧记忆""" + + # 1. 获取指定时间前的记忆 + entries = await self.backend.get_before(before_timestamp) + + if not entries: + return "" + + # 2. 构建摘要 + summary_prompt = f"""请简洁总结以下对话要点: + +{chr(10).join(f"- {e.content}" for e in entries)} + +保留关键信息: +- 主要话题或问题 +- 已确定的结论或方案 +- 未完成的任务 +""" + + # 3. 调用 LLM 摘要 + summary = await self._llm_summarize(summary_prompt) + + # 4. 创建摘要记忆 + summary_entry = MemoryEntry( + id=str(uuid.uuid4()), + type=MemoryType.SEMANTIC, + content=summary, + metadata={"original_entries": len(entries)}, + created_at=time.time(), + updated_at=time.time(), + importance=0.7, + ) + + await self.backend.save(summary_entry) + + # 5. 删除原始记忆 + for entry in entries: + await self.backend.delete(entry.id) + + return summary +``` + +--- + +## 10. 平台适配(Platform Adaptation) + +### 10.1 平台特性 + +```python +@dataclass +class PlatformCapabilities: + """平台能力""" + supports_streaming: bool = False + max_message_length: int = 4096 + supports_markdown: bool = True + supports_html: bool = False + supports_images: bool = True + supports_mentions: bool = True + supports_reply: bool = True + rate_limit_rpm: int = 60 + rate_limit_rpd: int = 10000 + +PLATFORM_CAPABILITIES = { + "telegram": PlatformCapabilities( + supports_streaming=False, + max_message_length=4096, + supports_markdown=True, + supports_html=True, + ), + "discord": PlatformCapabilities( + supports_streaming=False, + max_message_length=2000, + supports_markdown=True, + supports_html=False, + supports_reply=True, + ), + "qq": PlatformCapabilities( + supports_streaming=False, + max_message_length=500, + supports_markdown=False, + supports_mentions=True, + ), + "webchat": PlatformCapabilities( + supports_streaming=True, + max_message_length=10000, + supports_markdown=True, + supports_html=True, + ), +} +``` + +### 10.2 策略选择器 + +```python +class PlatformStrategySelector: + """平台策略选择器""" + + def __init__(self, config: PlatformAdaptationConfig): + self.config = config + self.capabilities = PLATFORM_CAPABILITIES + + def select_strategy( + self, + platform: str, + content_length: int, + user_preference: str | None = None, + ) -> OutputStrategy: + """选择输出策略""" + + caps = self.capabilities.get(platform) + + # 1. 用户偏好优先 + if user_preference and self._is_valid_strategy(user_preference, caps): + return OutputStrategy(user_preference) + + # 2. 平台能力判断 + if not caps: + return OutputStrategy.FULL + + # 3. 平台配置覆盖 + platform_strategy = self.config.strategy_by_platform.get(platform) + if platform_strategy: + return OutputStrategy(platform_strategy) + + # 4. 内容长度判断 + if content_length > caps.max_message_length: + return OutputStrategy.SEGMENTED + + # 5. 流式支持判断 + if caps.supports_streaming: + return OutputStrategy.STREAMING + + return OutputStrategy.FULL +``` + +--- + +## 11. 配置汇总 + +### 11.1 agent.yaml 完整配置 + +```yaml +# Agent 配置 + +# 输入缓冲区 +input_buffer: + max_queue_size: 1000 + max_message_age: 3600 + overflow_strategy: "drop_oldest" + overflow_hint: "[消息过多,部分早期消息已丢弃]" + +# 流控 +flow_control: + mode: "auto" + auto: + api_rpm_limit: 60 + messages_per_request: 5 + safety_margin: 0.8 + min_interval: 0.5 + max_interval: 10 + +# 上下文 +context: + max_context_tokens: 128000 + compress_threshold: 0.85 + keep_recent_messages: 6 + compress_instruction: | + 请简洁地总结对话要点... + +# 工具调用 +tool_calling: + strategy: "smart" + max_calls_per_request: 128 + timeout: 60 + max_retries: 3 + parallel_calls: true + max_parallel_calls: 5 + +# 安全 +security: + injection: + enable: true + mode: "strict" + patterns: [...] + on_detect: "sanitize" + content_filter: + enable: true + level: "standard" + replacement: "[已过滤]" + leakage_prevention: + blocked_file_patterns: [...] + blocked_output_patterns: [...] + placeholder: "[REDACTED]" + +# 输出 +output: + default_strategy: "streaming" + streaming: + chunk_size: 20 + chunk_interval: 0.05 + segmented: + enable: true + threshold: 500 + mode: "sentence" + split_regex: "[。!?;\n]+" + random_interval: "0.5,2.0" + add_ellipsis: true + platform_adaptation: + strategy_by_platform: + telegram: "segmented" + discord: "segmented" + webchat: "streaming" + max_length_by_platform: + telegram: 4096 + discord: 2000 + +# 记忆 +memory: + backend: "sqlite" + sqlite: + path: "$XDG_DATA_HOME/astrbot/state/memory.db" + retention: + working_memory_days: 7 + auto_summary_threshold: 50 + context_window: + recent_rounds: 10 +``` + +--- + +## 12. 错误处理与恢复 + +### 12.1 错误分类 + +```python +class ErrorType(Enum): + """错误类型""" + RATE_LIMIT = "rate_limit" # 限流 + TIMEOUT = "timeout" # 超时 + NETWORK = "network" # 网络错误 + API = "api" # API 错误 + TOOL = "tool" # 工具错误 + SECURITY = "security" # 安全错误 + INTERNAL = "internal" # 内部错误 + +@dataclass +class ErrorRecoveryConfig: + """错误恢复配置""" + max_retries: dict[ErrorType, int] = field(default_factory=lambda: { + ErrorType.RATE_LIMIT: 5, + ErrorType.TIMEOUT: 3, + ErrorType.NETWORK: 3, + ErrorType.API: 2, + ErrorType.TOOL: 2, + ErrorType.SECURITY: 0, + ErrorType.INTERNAL: 1, + }) + + backoff_multiplier: float = 1.5 + max_backoff: float = 60.0 +``` + +### 12.2 错误处理策略 + +```python +async def handle_error( + error: Exception, + context: AgentContext, + config: ErrorRecoveryConfig, +) -> ErrorAction: + """处理错误并决定下一步行动""" + + error_type = classify_error(error) + retries = context.metadata.get(f"retry_{error_type.value}", 0) + + if retries >= config.max_retries.get(error_type, 0): + return ErrorAction.FAIL + + # 指数退避 + if retries > 0: + backoff = min( + config.backoff_multiplier ** retries, + config.max_backoff + ) + await asyncio.sleep(backoff) + + context.metadata[f"retry_{error_type.value}"] = retries + 1 + + if error_type == ErrorType.RATE_LIMIT: + # 更新流控配置 + flow_control.decrease_rate(0.8) + return ErrorAction.RETRY + + elif error_type == ErrorType.SECURITY: + # 安全错误不重试 + return ErrorAction.BLOCK + + elif error_type == ErrorType.API: + # API 错误,检查是否可恢复 + if is_retryable_api_error(error): + return ErrorAction.RETRY + return ErrorAction.FAIL + + return ErrorAction.RETRY + +class ErrorAction(Enum): + """错误处理动作""" + RETRY = "retry" + FAIL = "fail" + BLOCK = "block" + FALLBACK = "fallback" +``` + +--- + +## 13. 扩展点 + +### 13.1 插件扩展点 + +```python +# 输入处理扩展 +class InputBufferPlugin(ABC): + """输入缓冲区插件""" + + async def pre_add_message( + self, + message: InputMessage, + ) -> InputMessage | None: + """消息添加前拦截,返回 None 表示跳过""" + pass + + async def post_add_message( + self, + message: InputMessage, + ) -> None: + """消息添加后处理""" + pass + +# 输出处理扩展 +class OutputBufferPlugin(ABC): + """输出缓冲区插件""" + + async def pre_send_message( + self, + message: OutputMessage, + ) -> OutputMessage | None: + """消息发送前拦截""" + pass + + async def post_send_message( + self, + message: OutputMessage, + ) -> None: + """消息发送后处理""" + pass + +# 安全扩展 +class SecurityPlugin(ABC): + """安全插件""" + + async def check_injection( + self, + content: str, + ) -> list[SecurityIssue]: + """自定义注入检测""" + pass + + async def filter_content( + self, + content: str, + ) -> str: + """自定义内容过滤""" + pass +``` + +### 13.2 调度器扩展 + +```python +# 自定义调度策略 +class CustomScheduler(ABC): + """自定义调度策略""" + + async def select_next_message( + self, + queues: dict[str, UserMessageQueue], + ) -> InputMessage | None: + """选择下一条消息""" + pass + + async def on_queue_empty( + self, + user_id: str, + ) -> None: + """队列为空时的处理""" + pass +``` diff --git a/openspec/api.md b/openspec/api.md new file mode 100644 index 000000000..b9cb2e4b5 --- /dev/null +++ b/openspec/api.md @@ -0,0 +1,651 @@ +# HTTP API 服务器规范 + +## 概述 + +AstrBot 提供 HTTP API 服务器,用于第三方应用集成。支持 API Key 认证,采用 RESTful 风格设计。 + +**特性**: +- API Key 认证 +- SSE 流式响应 +- 文件上传/下载 +- 主动消息推送 + +## 服务器配置 + +### 启动参数 + +| 参数 | 环境变量 | 说明 | +|------|----------|------| +| `--host` | `ASTRBOT_HOST` | 绑定地址,默认 `0.0.0.0` | +| `--port` | `ASTRBOT_PORT` | 绑定端口,默认 `6185` | +| `--ssl` | `ASTRBOT_SSL_ENABLE` | 启用 SSL/TLS | +| `--ssl-cert` | `ASTRBOT_SSL_CERT` | SSL 证书路径 | +| `--ssl-key` | `ASTRBOT_SSL_KEY` | SSL 私钥路径 | + +### 配置示例 + +```yaml +# system.yaml +api: + host: "0.0.0.0" + port: 6185 + ssl: + enable: true + cert: "/etc/astrbot/certs/fullchain.pem" + key: "/etc/astrbot/certs/privkey.pem" + # API 请求超时(秒) + timeout: 60 + # 最大请求体大小(MB) + max_body_size: 100 +``` + +## 认证 + +### API Key 认证 + +所有 `/api/v1/*` 端点需要 API Key 认证: + +| 方式 | 头部 | 示例 | +|------|------|------| +| Header | `X-API-Key` | `X-API-Key: sk-xxxxx` | +| Bearer | `Authorization` | `Authorization: Bearer sk-xxxxx` | + +**API Key 配置**: + +```yaml +# secrets.yaml +api: + keys: + - id: "my-app" + key: "sk-xxxxxxxxxxxx" + name: "My Application" + permissions: + - "chat" + - "file" + - "message" + rate_limit: + requests: 100 + period: 60 # seconds +``` + +### 权限说明 + +| 权限 | 说明 | +|------|------| +| `chat` | 聊天功能(发送消息、查询会话) | +| `file` | 文件上传/下载 | +| `message` | 主动消息推送 | +| `admin` | 管理功能(预留) | + +## 错误响应 + +### 错误格式 + +```json +{ + "status": "error", + "code": 401, + "message": "Invalid API key", + "request_id": "req-abc123" +} +``` + +### HTTP 状态码 + +| 状态码 | 说明 | +|--------|------| +| `200` | 请求成功 | +| `400` | 请求参数错误 | +| `401` | 未授权(API Key 无效) | +| `403` | 禁止访问(权限不足) | +| `404` | 资源不存在 | +| `429` | 请求过于频繁(限流) | +| `500` | 服务器内部错误 | +| `503` | 服务不可用 | + +### 错误码 + +| 码值 | 说明 | +|------|------| +| `invalid_api_key` | API Key 无效 | +| `expired_api_key` | API Key 已过期 | +| `insufficient_permissions` | 权限不足 | +| `rate_limit_exceeded` | 请求频率超限 | +| `invalid_parameter` | 参数错误 | +| `session_not_found` | 会话不存在 | +| `config_not_found` | 配置不存在 | +| `file_not_found` | 文件不存在 | +| `upload_failed` | 文件上传失败 | +| `platform_error` | 平台错误 | + +## 端点列表 + +| 方法 | 路径 | 说明 | +|------|------|------| +| `GET` | `/api/v1/im/bots` | 获取机器人列表 | +| `POST` | `/api/v1/file` | 上传文件 | +| `GET` | `/api/v1/file` | 下载文件 | +| `POST` | `/api/v1/chat` | 发送聊天消息(SSE) | +| `GET` | `/api/v1/chat/sessions` | 获取会话列表 | +| `POST` | `/api/v1/im/message` | 发送主动消息 | +| `GET` | `/api/v1/configs` | 获取配置列表 | + +--- + +## API 端点详情 + +### GET /api/v1/im/bots + +获取已配置的机器人/平台 ID 列表。 + +**请求** + +``` +GET /api/v1/im/bots +X-API-Key: sk-xxxxx +``` + +**响应** + +```json +{ + "status": "ok", + "data": { + "bot_ids": [ + "telegram:bot:123456789", + "discord:bot:987654321", + "webchat:FriendMessage:openapi_probe" + ] + } +} +``` + +--- + +### POST /api/v1/file + +上传文件,获取 `attachment_id` 供后续使用。 + +**请求** + +``` +POST /api/v1/file +X-API-Key: sk-xxxxx +Content-Type: multipart/form-data + +file: +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file` | binary | 是 | 上传的文件 | + +**响应** + +```json +{ + "status": "ok", + "data": { + "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111", + "filename": "document.pdf", + "type": "application/pdf", + "size": 102400 + } +} +``` + +**限制**: +- 最大文件大小:100MB +- 支持格式:图片、音频、视频、文档等 + +--- + +### GET /api/v1/file + +下载已上传的文件。 + +**请求** + +``` +GET /api/v1/file?attachment_id=9a2f8c72-e7af-4c0e-b352-111111111111 +X-API-Key: sk-xxxxx +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `attachment_id` | string | 是 | 上传时返回的附件 ID | + +**响应** + +``` +200 OK +Content-Type: application/octet-stream + + +``` + +--- + +### POST /api/v1/chat + +发送聊天消息,支持 SSE 流式响应。 + +**请求** + +``` +POST /api/v1/chat +X-API-Key: sk-xxxxx +Content-Type: application/json + +{ + "message": "Hello, how are you?", + "username": "alice", + "session_id": "my_session_001", + "enable_streaming": true, + "selected_provider": "openai_chat_completion", + "selected_model": "gpt-4.1-mini" +} +``` + +**请求体参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `message` | string \| array | 是 | 消息内容或消息链 | +| `username` | string | 是 | 用户名 | +| `session_id` | string | 否 | 会话 ID,不提供则自动创建 UUID | +| `conversation_id` | string | 否 | `session_id` 的别名 | +| `selected_provider` | string | 否 | 指定 LLM 提供商 | +| `selected_model` | string | 否 | 指定模型 | +| `enable_streaming` | boolean | 否 | 启用 SSE 流式响应,默认 `true` | +| `config_id` | string | 否 | 指定配置文件 ID | +| `config_name` | string | 否 | 指定配置文件名称(当 config_id 未提供时) | + +**消息链格式** + +```json +{ + "message": [ + { "type": "plain", "text": "Please analyze this file" }, + { "type": "file", "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" } + ] +} +``` + +**消息类型** + +| 类型 | 说明 | 字段 | +|------|------|------| +| `plain` | 文本消息 | `text` | +| `reply` | 回复消息 | `text`, `message_id` | +| `image` | 图片 | `attachment_id` 或 `url` | +| `file` | 文件 | `attachment_id`, `filename` | +| `record` | 语音 | `attachment_id` | +| `video` | 视频 | `attachment_id` | + +**SSE 流式响应** + +``` +200 OK +Content-Type: text/event-stream + +event: message +data: {"type": "message", "content": "Hello"} + +event: message +data: {"type": "message", "content": " world"} + +event: done +data: {"type": "done", "message_id": "msg-123"} + +event: error +data: {"type": "error", "message": "Error description"} +``` + +**SSE 事件类型** + +| 事件 | 说明 | 数据格式 | +|------|------|----------| +| `message` | 消息片段 | `{"type": "message", "content": "..."}` | +| `tool_call` | 工具调用 | `{"type": "tool_call", "tool": "name", "args": {...}}` | +| `tool_result` | 工具结果 | `{"type": "tool_result", "tool": "name", "result": {...}}` | +| `done` | 完成 | `{"type": "done", "message_id": "..."}` | +| `error` | 错误 | `{"type": "error", "message": "..."}` | + +**完整响应(非流式)** + +```json +{ + "status": "ok", + "data": { + "message_id": "msg-123", + "content": "Hello! How can I help you today?", + "sender": { + "user_id": "alice", + "nickname": "Alice" + }, + "timestamp": "2026-03-26T10:00:00Z" + } +} +``` + +--- + +### GET /api/v1/chat/sessions + +获取用户的会话列表(分页)。 + +**请求** + +``` +GET /api/v1/chat/sessions?username=alice&page=1&page_size=20 +X-API-Key: sk-xxxxx +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `username` | string | 是 | 用户名 | +| `page` | integer | 否 | 页码,默认 `1` | +| `page_size` | integer | 否 | 每页数量,默认 `20`,最大 `100` | +| `platform_id` | string | 否 | 平台过滤器 | + +**响应** + +```json +{ + "status": "ok", + "data": { + "sessions": [ + { + "session_id": "my_session_001", + "platform_id": "webchat:FriendMessage:openapi_probe", + "creator": "alice", + "display_name": "Alice", + "is_group": 0, + "created_at": "2026-03-26T10:00:00Z", + "updated_at": "2026-03-26T10:30:00Z" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + } +} +``` + +--- + +### POST /api/v1/im/message + +向平台机器人发送主动消息。 + +**请求** + +``` +POST /api/v1/im/message +X-API-Key: sk-xxxxx +Content-Type: application/json + +{ + "umo": "webchat:FriendMessage:openapi_probe", + "message": "ping from api" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `umo` | string | 是 | 统一消息源地址 | +| `message` | string \| array | 是 | 消息内容或消息链 | + +**UMO 格式** + +``` +platform:message_type:session_id +``` + +| 平台 | 示例 | +|------|------| +| `webchat` | `webchat:FriendMessage:openapi_probe` | +| `telegram` | `telegram:private:123456789` | +| `telegram` | `telegram:group:-123456789` | +| `discord` | `discord:channel:987654321` | + +**消息类型** + +| 类型 | 说明 | +|------|------| +| `private` | 私聊 | +| `group` | 群聊 | +| `FriendMessage` | WebChat 好友消息 | + +**响应** + +```json +{ + "status": "ok", + "message": "Message sent successfully" +} +``` + +--- + +### GET /api/v1/configs + +获取可用的聊天配置文件列表。 + +**请求** + +``` +GET /api/v1/configs +X-API-Key: sk-xxxxx +``` + +**响应** + +```json +{ + "status": "ok", + "data": { + "configs": [ + { + "id": "default", + "name": "默认配置", + "path": "/var/lib/astrbot/data/configs/default.yaml", + "is_default": true + }, + { + "id": "coding-assistant", + "name": "编程助手", + "path": "/var/lib/astrbot/data/configs/coding-assistant.yaml", + "is_default": false + } + ] + } +} +``` + +--- + +## 速率限制 + +| 等级 | 请求数 | 窗口 | +|------|--------|------| +| 默认 | 100 | 60 秒 | +| 高级 | 1000 | 60 秒 | + +**限流响应** + +``` +429 Too Many Requests +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1742992060 + +{ + "status": "error", + "code": 429, + "message": "Rate limit exceeded. Try again in 30 seconds." +} +``` + +--- + +## Webhook 回调 + +### 配置 + +```yaml +# system.yaml +callback_api_base: "https://your-server.com/callback" +``` + +### 回调事件 + +| 事件 | 说明 | +|------|------| +| `message.received` | 收到消息 | +| `message.sent` | 消息发送成功 | +| `session.created` | 会话创建 | +| `session.updated` | 会话更新 | +| `error` | 错误发生 | + +### 回调签名 + +``` +X-AstrBot-Signature: sha256=xxxxx +X-AstrBot-Timestamp: 1742992060 +``` + +验证签名: + +```python +import hmac +import hashlib + +def verify_signature(payload: bytes, timestamp: str, signature: str, secret: str) -> bool: + expected = hmac.new( + secret.encode(), + f"{timestamp}.".encode() + payload, + hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(f"sha256={expected}", signature) +``` + +--- + +## 健康检查 + +### GET /health + +``` +GET /health +``` + +**响应** + +```json +{ + "status": "healthy", + "version": "4.16.0", + "uptime": 3600 +} +``` + +### GET /ready + +``` +GET /ready +``` + +**响应** + +```json +{ + "status": "ready", + "components": { + "api": "ok", + "platforms": { + "telegram": "ok", + "discord": "ok" + }, + "llm_providers": { + "openai": "ok" + } + } +} +``` + +--- + +## 客户端 SDK + +### Python + +```python +from astrbot.api.http import AstrBotClient + +client = AstrBotClient(api_key="sk-xxxxx", base_url="http://localhost:6185") + +# 发送消息 +async def main(): + async for event in client.chat("Hello!", username="alice", session_id="test"): + print(event) + + # 或非流式 + result = await client.chat_one("Hello!", username="alice") + print(result) +``` + +### JavaScript + +```javascript +import { AstrBotClient } from "@astrbot/sdk"; + +const client = new AstrBotClient({ + apiKey: "sk-xxxxx", + baseUrl: "http://localhost:6185" +}); + +// 流式 +for await (const event of client.chat({ message: "Hello!", username: "alice" })) { + console.log(event); +} + +// 非流式 +const result = await client.chatOne({ message: "Hello!", username: "alice" }); +``` + +--- + +## 目录结构 + +``` +$XDG_CONFIG_HOME/astrbot/ +├── config.yaml # 主配置入口 +├── secrets.yaml # 安全配置(包含 API Keys) +└── ... +``` + +--- + +## 安全建议 + +1. **API Key 安全** + - API Key 仅通过 HTTPS 传输 + - 定期轮换 API Key + - 不同应用使用不同 Key + +2. **网络隔离** + - 生产环境使用 SSL/TLS + - 限制 API 服务器访问范围 + - 使用防火墙或 VPN + +3. **权限控制** + - 按最小权限原则分配 API Key 权限 + - 避免使用 `admin` 权限 + - 记录 API 调用日志 + +4. **限流配置** + - 根据业务需求调整限流参数 + - 监控限流触发情况 + - 合理设置不同等级的限流策略 diff --git a/openspec/config.md b/openspec/config.md new file mode 100644 index 000000000..b49c50f5a --- /dev/null +++ b/openspec/config.md @@ -0,0 +1,565 @@ +# 配置规范 + +## 概述 + +AstrBot 配置系统遵循以下原则: + +| 原则 | 说明 | +|------|------| +| **分离关注点** | 按功能域分离配置 | +| **敏感信息隔离** | 密钥、密码存储在独立的安全文件中 | +| **环境变量优先** | 密钥通过环境变量注入,不写入配置文件 | +| **分层配置** | 系统配置 → 平台配置 → 插件配置 | + +## 配置分类 + +### 配置域 + +| 域 | 配置文件 | 说明 | +|---|----------|------| +| **安全配置** | `secrets.yaml` | API keys、密码、JWT secrets、GPG 密钥 | +| **平台配置** | `platform.yaml` | 平台适配器设置 | +| **提供商配置** | `providers.yaml` | LLM 提供商设置(不含密钥) | +| **Agent 配置** | `agent.yaml` | Agent 行为参数 | +| **系统配置** | `system.yaml` | 日志、调试、代理、API 服务器等 | +| **GPG 配置** | `gpg.yaml` | GPG 安全配置(可选) | +| **插件配置** | `plugins/` | 各插件独立配置 | + +## GPG 安全配置(可选) + +### 概述 + +AstrBot 提供可选的 GPG 安全功能,用于配置文件的签名与验签,防止配置被篡改。 + +| 功能 | 说明 | +|------|------| +| **配置签名** | 对配置文件进行签名,确保完整性 | +| **配置验签** | 启动时验证配置文件签名,检测篡改 | +| **插件签名** | 验证插件包的 GPG 签名 | + +### gpg.yaml(GPG 配置) + +```yaml +# GPG 安全配置 +# 可选功能,不启用时不影响正常功能 + +security: + # GPG 功能开关 + enable: false + + # 验签模式 + # - none: 不验签 + # - warn: 验签失败仅警告 + # - enforce: 验签失败阻止启动 + verify_mode: "warn" + + # 信任的公钥指纹列表 + trusted_keys: + - "ABCDEF1234567890..." + - "FEDCBA0987654321..." + + # 签名的配置文件模式 + signed_configs: + - "secrets.yaml" + - "agent.yaml" + - "platform.yaml" + + # 签名文件后缀 + signature_suffix: ".sig" + + # GPG 主目录(默认使用系统配置) + gnupg_home: "" +``` + +### secrets.yaml 中的 GPG 密钥 + +```yaml +# GPG 密钥配置 +gpg: + # 私钥(用于签名) + # 使用空字符串表示不配置 + private_key: "" + passphrase: "" + + # 或使用 GPG Keybox 格式 + # keybox: "/path/to/s.gpg-keybox" + + # 公钥指纹(用于验签) + fingerprint: "ABCDEF1234567890..." +``` + +### 配置签名与验签流程 + +#### 签名流程 + +```bash +# 1. 首次签名 +astrbot gpg sign --config secrets.yaml +astrbot gpg sign --config agent.yaml + +# 2. 生成签名文件 +# secrets.yaml.sig +# agent.yaml.sig + +# 3. 签名文件内容(JSON 格式) +{ + "config_file": "secrets.yaml", + "signature": "-----BEGIN PGP SIGNATURE-----...", + "signed_at": "2026-03-26T10:00:00Z", + "issuer": "AstrBot/1.0" +} +``` + +#### 验签流程 + +``` +启动时: +1. 检查 gpg.yaml 中 verify_mode +2. 对 signed_configs 中的每个文件: + a. 读取配置文件 + b. 读取对应的 .sig 签名文件 + c. 使用 trusted_keys 中的公钥验签 +3. 根据 verify_mode 处理结果: + - none: 跳过验签 + - warn: 验签失败记录警告日志 + - enforce: 验签失败阻止启动并报错 +``` + +### 目录结构 + +``` +$XDG_CONFIG_HOME/astrbot/ +├── gpg.yaml # GPG 安全配置 +├── secrets.yaml # 包含 GPG 密钥 +├── secrets.yaml.sig # 配置文件签名 +├── agent.yaml +├── agent.yaml.sig +└── ... +``` + +### 命令行接口 + +```bash +# 签名配置文件 +astrbot gpg sign --config [--output ] + +# 验签配置文件 +astrbot gpg verify --config + +# 验签所有已签名配置 +astrbot gpg verify --all + +# 导出公钥 +astrbot gpg export --keyring [--output ] + +# 生成新密钥对 +astrbot gpg generate --name "AstrBot" --email "astrbot@example.com" +``` + +### 安全原则 + +| 原则 | 说明 | +|------|------| +| **私钥保护** | 私钥仅存储在 secrets.yaml 中,不提交到版本控制 | +| **密钥分离** | GPG 签名密钥与 API 密钥分离存储 | +| **验签优先** | 生产环境建议使用 `verify_mode: enforce` | +| **密钥轮换** | 定期更换签名密钥并重新签名配置文件 | + +## 敏感信息隔离 + +### 原则 + +| 原则 | 说明 | +|------|------| +| **密钥不上传** | `secrets.yaml` 必须在 `.gitignore` 中 | +| **环境变量注入** | 生产环境通过环境变量注入密钥 | +| **不记录日志** | 配置值不写入日志 | + +### secrets.yaml + +```yaml +# 安全配置 - 请勿提交到版本控制 +# Security config - DO NOT commit to version control + +# Dashboard 凭证 +dashboard: + password: "your-hashed-password" + jwt_secret: "your-jwt-secret" + +# LLM Provider API Keys +providers: + openai: + api_key: "sk-..." + anthropic: + api_key: "sk-ant-..." + dashscope: + api_key: "..." + +# 平台凭证 +platforms: + telegram: + bot_token: "12345:ABC..." + discord: + bot_token: "..." + +# 第三方服务 +third_party: + baidu_aip: + app_id: "" + api_key: "" + secret_key: "" + moonshotai: + api_key: "" + coze: + api_key: "" + bot_id: "" + +# API Keys(HTTP API 服务认证) +api: + keys: + - id: "my-app" + key: "sk-xxxxxxxxxxxx" + name: "My Application" + permissions: + - "chat" + - "file" + - "message" + rate_limit: + requests: 100 + period: 60 +``` + +### 环境变量注入 + +敏感配置通过环境变量覆盖: + +```bash +# .env 或系统环境变量 +ASTRBOT_DASHBOARD_PASSWORD=xxx +ASTRBOT_DASHBOARD_JWT_SECRET=xxx +ASTRBOT_PROVIDER_OPENAI_API_KEY=sk-xxx +ASTRBOT_PROVIDER_ANTHROPIC_API_KEY=sk-ant-xxx +ASTRBOT_PLATFORM_TELEGRAM_BOT_TOKEN=xxx +``` + +## 配置文件结构 + +### 1. system.yaml(系统配置) + +```yaml +# 系统配置 + +log: + level: INFO # DEBUG, INFO, WARNING, ERROR + file_enable: false + file_path: logs/astrbot.log + file_max_mb: 20 + disable_access_log: true + +proxy: + http_proxy: "" + https_proxy: "" + no_proxy: + - localhost + - 127.0.0.1 + +trace: + enable: false + log_enable: false + log_path: logs/astrbot.trace.log + +temp: + dir_max_size: 1024 # MB + +timezone: Asia/Shanghai + +pypi_index_url: https://mirrors.aliyun.com/pypi/simple/ +pip_install_arg: "" + +# 回调配置 +callback_api_base: "" + +# API 服务器配置 +api: + host: "0.0.0.0" + port: 6185 + timeout: 60 + max_body_size: 100 + rate_limit: + default: + requests: 100 + period: 60 + high: + requests: 1000 + period: 60 +``` + +### 2. platform.yaml(平台配置) + +```yaml +# 平台配置 + +# 通用平台设置 +platform_settings: + unique_session: false + rate_limit: + time: 60 + count: 30 + strategy: "stall" # stall, reject + reply_prefix: "" + forward_threshold: 1500 + enable_id_white_list: true + id_whitelist: [] + id_whitelist_log: true + wl_ignore_admin_on_group: true + wl_ignore_admin_on_friend: true + reply_with_mention: false + reply_with_quote: false + path_mapping: [] + segmented_reply: + enable: false + only_llm_result: true + interval_method: "random" + interval: "1.5,3.5" + words_count_threshold: 150 + split_mode: "regex" + regex: ".*?[。?!~…]+|.+$" + split_words: ["。", "?", "!", "~", "…"] + no_permission_reply: true + empty_mention_waiting: true + empty_mention_waiting_need_reply: true + friend_message_needs_wake_prefix: false + ignore_bot_self_message: false + ignore_at_all: false + +# 平台列表 +platforms: [] + +# 平台特定配置 +platform_specific: + lark: + pre_ack_emoji: + enable: false + emojis: ["Typing"] + telegram: + pre_ack_emoji: + enable: false + emojis: ["✍️"] + discord: + pre_ack_emoji: + enable: false + emojis: ["🤔"] +``` + +### 3. providers.yaml(提供商配置) + +```yaml +# LLM 提供商配置 + +provider_settings: + enable: true + default_provider_id: "" + fallback_chat_models: [] + default_image_caption_provider_id: "" + image_caption_prompt: "Please describe the image using Chinese." + provider_pool: ["*"] + +# API Keys 在 secrets.yaml 中配置 +# providers: +# openai: +# api_key: "sk-..." # 在 secrets.yaml 中 +# model: "gpt-4" +# base_url: "" # 可选,自定义 endpoint +``` + +### 4. agent.yaml(Agent 配置) + +```yaml +# Agent 配置 + +agent_settings: + wake_prefix: ["/"] + default_personality: "default" + persona_pool: ["*"] + prompt_prefix: "{{prompt}}" + + # 上下文配置 + context_limit_reached_strategy: "truncate_by_turns" + llm_compress_instruction: "..." + llm_compress_keep_recent: 6 + llm_compress_provider_id: "" + max_context_length: -1 + dequeue_context_length: 1 + + # 输出配置 + streaming_response: false + show_tool_use_status: false + show_tool_call_result: false + sanitize_context_by_modalities: false + max_quoted_fallback_images: 20 + + # Web Search + web_search: false + websearch_provider: "default" + # websearch_tavily_key: 在 secrets.yaml 中 + # websearch_bocha_key: 在 secrets.yaml 中 + web_search_link: false + + # 标识 + identifier: false + group_name_display: false + datetime_system_prompt: true + + # Agent Runner + agent_runner_type: "local" # local, dify, coze, dashscope, deerflow + dify_agent_runner_provider_id: "" + coze_agent_runner_provider_id: "" + dashscope_agent_runner_provider_id: "" + deerflow_agent_runner_provider_id: "" + + # 工具配置 + unsupported_streaming_strategy: "realtime_segmenting" + reachability_check: false + max_agent_step: 30 + tool_call_timeout: 60 + tool_schema_mode: "full" + + # 安全模式 + llm_safety_mode: true + safety_mode_strategy: "system_prompt" + + # 主动能力 + proactive_capability: + add_cron_tools: true + + # Computer Use + computer_use_runtime: "none" + computer_use_require_admin: true + + # 图片压缩 + image_compress_enabled: true + image_compress_options: + max_size: 1024 + quality: 95 + +# 子 Agent 编排 +subagent_orchestrator: + main_enable: false + remove_main_duplicate_tools: false + router_system_prompt: "..." + agents: [] + +# STT/TTS +provider_stt_settings: + enable: false + provider_id: "" + +provider_tts_settings: + enable: false + provider_id: "" + dual_output: false + use_file_service: false + trigger_probability: 1.0 + +# LTM +provider_ltm_settings: + group_icl_enable: false + group_message_max_cnt: 300 + image_caption: false + image_caption_provider_id: "" + active_reply: + enable: false + method: "possibility_reply" + possibility_reply: 0.1 + whitelist: [] +``` + +### 5. plugins/(插件配置) + +各插件配置在 `plugins/` 目录下,文件名与插件名对应: + +``` +plugins/ +├── _conf_schema.json # 插件配置 schema +├── my_plugin.yaml # my_plugin 插件配置 +└── another_plugin.yaml # another_plugin 插件配置 +``` + +## 目录结构 + +``` +$XDG_CONFIG_HOME/astrbot/ +├── config.yaml # 主配置入口 +├── secrets.yaml # 安全配置(不提交) +├── system.yaml # 系统配置 +├── platform.yaml # 平台配置 +├── providers.yaml # 提供商配置 +├── agent.yaml # Agent 配置 +├── gpg.yaml # GPG 安全配置(可选) +├── plugins/ # 插件配置 +│ ├── _conf_schema.json # 插件 schema +│ └── *.yaml # 各插件配置 +└── mcp_servers.json # MCP 服务器配置 +``` + +## 配置加载优先级 + +``` +环境变量 > secrets.yaml > config.yaml > 默认值 +``` + +| 优先级 | 来源 | 说明 | +|--------|------|------| +| 最高 | 环境变量 | `ASTRBOT_*` 前缀 | +| 高 | secrets.yaml | 敏感信息 | +| 中 | config.yaml | 用户配置 | +| 低 | 默认值 | 代码中的默认值 | + +## 配置迁移 + +旧版 `cmd_config.json` 迁移到新版: + +| 旧字段 | 新配置文件 | 说明 | +|--------|------------|------| +| `dashboard.*` | `secrets.yaml` | 密码、JWT secret | +| `provider_settings.websearch_*_key` | `secrets.yaml` | API keys | +| `platform_settings.*` | `platform.yaml` | 平台设置 | +| `provider_settings.*` | `providers.yaml` + `agent.yaml` | 提供商和 Agent | +| `provider_stt/tts_settings.*` | `agent.yaml` | STT/TTS | +| `content_safety.baidu_aip.*` | `secrets.yaml` | 安全密钥 | +| `log_level`, `http_proxy` | `system.yaml` | 系统配置 | +| `plugin_set`, `kb_*` | `plugins/` | 插件配置 | + +## 不规范示例 + +❌ **旧方式** - 所有配置混在一起: + +```json +{ + "dashboard": { + "password": "plain-text-or-hashed", + "jwt_secret": "secret-in-config" + }, + "provider_settings": { + "openai_api_key": "sk-xxx" + } +} +``` + +✅ **新方式** - 分离关注点: + +``` +config/ +├── secrets.yaml # 密码、密钥 +├── system.yaml # 日志、代理 +├── platform.yaml # 平台设置 +├── providers.yaml # 提供商配置 +└── agent.yaml # Agent 配置 +``` + +## 实施建议 + +1. **新增配置**:新功能应按域分类到对应文件 +2. **敏感信息**:绝不写入 `config.yaml`,使用 `secrets.yaml` 或环境变量 +3. **默认值**:在代码中设置默认值,配置文件仅覆盖需要修改的项 +4. **Schema 验证**:使用 JSON Schema 验证配置文件格式 diff --git a/openspec/env.md b/openspec/env.md index ddbdf7a00..574c7b9c0 100644 --- a/openspec/env.md +++ b/openspec/env.md @@ -155,6 +155,58 @@ --- +## API 服务器配置 / API Server + +### `ASTRBOT_API_ENABLE` + +| 属性 | 值 | +|------|-----| +| 说明 | 启用 HTTP API 服务器 | +| 类型 | boolean | +| 默认 | `true` | + +### `ASTRBOT_API_TIMEOUT` + +| 属性 | 值 | +|------|-----| +| 说明 | API 请求超时时间(秒) | +| 类型 | integer | +| 默认 | `60` | + +### `ASTRBOT_API_MAX_BODY_SIZE` + +| 属性 | 值 | +|------|-----| +| 说明 | API 最大请求体大小(MB) | +| 类型 | integer | +| 默认 | `100` | + +### `ASTRBOT_API_KEY` + +| 属性 | 值 | +|------|-----| +| 说明 | 默认 API Key(用于开发环境) | +| 类型 | string | +| 注意 | 建议通过 secrets.yaml 配置 | + +### `ASTRBOT_CALLBACK_API_BASE` + +| 属性 | 值 | +|------|-----| +| 说明 | Webhook 回调基础 URL | +| 类型 | url | +| 示例 | `https://your-server.com/callback` | + +### `ASTRBOT_CALLBACK_SECRET` + +| 属性 | 值 | +|------|-----| +| 说明 | Webhook 回调签名密钥 | +| 类型 | string | +| 注意 | 用于验证回调请求签名 | + +--- + ## SSL/TLS 配置 ### `ASTRBOT_SSL_ENABLE` @@ -268,6 +320,67 @@ --- +## GPG 安全配置 / GPG Security + +### `ASTRBOT_GPG_ENABLE` + +| 属性 | 值 | +|------|-----| +| 说明 | 启用 GPG 配置签名与验签功能 | +| 类型 | boolean | +| 默认 | `false` | + +### `ASTRBOT_GPG_VERIFY_MODE` + +| 属性 | 值 | +|------|-----| +| 说明 | 验签模式 | +| 类型 | enum | +| 默认 | `warn` | +| 可选值 | `none` (不验签), `warn` (仅警告), `enforce` (阻止启动) | + +### `ASTRBOT_GPG_TRUSTED_KEYS` + +| 属性 | 值 | +|------|-----| +| 说明 | 信任的 GPG 公钥指纹列表(逗号分隔) | +| 类型 | string | +| 示例 | `ABCDEF1234567890...,FEDCBA0987654321...` | + +### `ASTRBOT_GPG_SIGNED_CONFIGS` + +| 属性 | 值 | +|------|-----| +| 说明 | 需要验签的配置文件列表(逗号分隔) | +| 类型 | string | +| 示例 | `secrets.yaml,agent.yaml,platform.yaml` | + +### `ASTRBOT_GPG_GNUPG_HOME` + +| 属性 | 值 | +|------|-----| +| 说明 | GPG 主目录路径(默认使用系统配置) | +| 类型 | path | +| 示例 | `/home/user/.gnupg` | + +### `ASTRBOT_GPG_PRIVATE_KEY` + +| 属性 | 值 | +|------|-----| +| 说明 | GPG 私钥(ASCII Armor 格式),用于签名 | +| 类型 | string | +| 注意 | 建议通过 secrets.yaml 配置,优先使用环境变量 | + +### `ASTRBOT_GPG_PASSPHRASE` + +| 属性 | 值 | +|------|-----| +| 说明 | GPG 私钥密码短语 | +| 类型 | string | +| 注意 | 建议通过 secrets.yaml 配置,优先使用环境变量 | + +--- + ## 命名规范 ### 前缀规则 @@ -276,6 +389,7 @@ |------|------| | `ASTRBOT_` | AstrBot 核心配置 | | `ASTRBOT_SSL_` | SSL/TLS 配置 | +| `ASTRBOT_GPG_` | GPG 安全配置 | | `ASTRBOT_INSTANCE_` | 实例相关 | | `http_proxy`, `https_proxy`, `no_proxy` | 标准代理变量(无前缀) | diff --git a/openspec/path.md b/openspec/path.md index 730066583..c03145e7f 100644 --- a/openspec/path.md +++ b/openspec/path.md @@ -42,6 +42,7 @@ AstrBot 遵循 [XDG Base Directory Specification](https://specifications.freedes ``` /var/lib/astrbot/ ├── config/ # 配置文件 +│ └── mcp_servers.json # MCP 服务器配置 ├── data/ # 插件、skills 等 │ ├── plugins/ │ ├── plugin_data/ @@ -62,6 +63,36 @@ AstrBot 遵循 [XDG Base Directory Specification](https://specifications.freedes └── pid # PID 文件 ``` +### MCP 服务器配置 + +MCP 服务器配置位于 `config/mcp_servers.json`: + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], + "env": {} + }, + "http-server": { + "url": "http://localhost:3000/mcp", + "transport": "sse" + } + } +} +``` + +**配置字段**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `command` | string | 启动命令(stdio 传输) | +| `args` | array | 命令参数 | +| `env` | object | 环境变量 | +| `url` | string | 服务器 URL(HTTP 传输) | +| `transport` | string | 传输类型:`stdio` / `sse` / `streamable_http` | + ## 平台差异 | 平台 | 配置目录 | 数据目录 | 缓存目录 | 运行时目录 | @@ -86,7 +117,6 @@ AstrBot 遵循 [XDG Base Directory Specification](https://specifications.freedes ``` $XDG_DATA_HOME/astrbot/ ├── data/ # 应用数据 -│ ├── config/ # 配置文件 │ ├── plugins/ # 插件目录 │ ├── plugin_data/ # 插件数据 │ ├── skills/ # Skills 目录 @@ -101,7 +131,8 @@ $XDG_DATA_HOME/astrbot/ $XDG_CONFIG_HOME/astrbot/ ├── .env # 环境变量 -└── config.yaml # 主配置文件 +├── config.yaml # 主配置文件 +└── mcp_servers.json # MCP 服务器配置 ``` ## 路径映射 @@ -283,6 +314,47 @@ $XDG_DATA_HOME/astrbot// 所有平台统一使用 `ASTRBOT_*` 环境变量覆盖默认路径,优先级最高。 +## 与 Claude Code 目录对比 + +Claude Code 使用 `~/.claude/` 作为根目录,**不遵循 XDG 规范**: + +``` +~/.claude/ # Claude Code 根目录 +├── backups/ # 备份 +├── cache/ # 缓存 +├── downloads/ # 下载 +├── file-history/ # 文件历史 +├── paste-cache/ # 粘贴缓存 +├── plans/ # 计划 +├── plugins/ # 插件 +├── projects/ # 项目会话 +├── session-env/ # 会话环境 +├── sessions/ # 会话 +├── shell-snapshots/ # Shell 快照 +├── skills/ # Skills +├── tasks/ # 任务 +├── teams/ # 团队 +├── history.jsonl # 对话历史 +├── settings.json # 设置 +└── settings.local.json # 本地设置 +``` + +### 关键差异 + +| 维度 | Claude Code | AstrBot | +|------|-------------|---------| +| 规范 | 非标准(固定 `~/.claude/`) | XDG 规范 | +| 配置 | 根目录直接放配置文件 | 分离到 `$XDG_CONFIG_HOME/` | +| 缓存 | 根目录下 `cache/` | `$XDG_CACHE_HOME/` | +| 数据 | 根目录直接放数据文件 | 分离到 `$XDG_DATA_HOME/` | +| 会话 | `sessions/`, `session-env/` | `$XDG_STATE_HOME/` | +| 项目 | `projects/` | 内嵌在数据目录 | + +### 设计哲学 + +- **Claude Code**:简单直接,所有内容在 `~/.claude/` 下 +- **AstrBot**:遵循 XDG,数据分类清晰(配置/数据/缓存/状态分离) + ## 已知限制 - Windows 平台**不遵循** XDG 规范,使用 Windows 标准路径