diff --git a/README.md b/README.md index 19b3a42..dac73e2 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,8 @@ Agent 执行层 (Agent) ◄───────────────── - src/client:WebSocket CLI 客户端实现 - src/command:命令系统,包括处理器、适配器、上下文和响应处理 - src/config:配置解析与默认值定义 -- src/domain:领域模型,包含消息和工具定义 +- src/domain:领域模型,包含消息、工具定义和能力策略(CapabilityPolicy) +- src/experts:专家系统,支持基于文件系统的专家发现、加载和运行时管理 - src/gateway:网关生命周期、InboundProcessor、OutboundDispatcher、SessionManager,以及消息执行、调度任务执行、Prompt 注入、历史压缩和记忆维护编排 - src/providers:不同 LLM Provider 的统一抽象,当前支持 openai 和 anthropic - src/tools:内置工具集合与 ToolRegistry @@ -502,17 +503,88 @@ tools 配置示例: 注意:bash 和 shell 是同一个工具在不同平台上的名称,运行时自动检测。 -## 8. 子代理系统 +## 8. 专家系统 + +PicoBot 支持基于文件系统的专家系统,用于给主智能体注入特定角色的行为语气和系统能力配置。专家类似于技能,但作用于主智能体本身,而非作为子任务执行。 + +### 8.1 专家发现位置 + +按从低到高优先级合并,后加载来源可覆盖同名专家: + +- 用户级:`~/.picobot/experts/*/EXPERT.md` +- 项目级:`.picobot/experts/*/EXPERT.md` + +### 8.2 EXPERT.md 格式 + +```md +--- +description: Rust 架构审查专家 +name: rust-reviewer # 可选,省略时使用目录名 +allowed_skills: [code-review-skill] # 可选,技能白名单(仅这些技能可用) +denied_skills: [shell-exec] # 可选,技能黑名单(这些技能被禁用) +allowed_tools: [read, bash, web_fetch] # 可选,工具白名单(仅这些工具可用) +denied_tools: [task] # 可选,工具黑名单(这些工具被禁用) +allowed_subagents: [general] # 可选,子代理白名单(仅允许创建这些子代理类型) +denied_subagents: [explore] # 可选,子代理黑名单(禁止创建这些子代理类型) +provider: deepseek # 可选,覆盖 provider(引用 config.json 的 providers 表) +model: deepseek-chat # 可选,覆盖 model(引用 config.json 的 models 表) +--- + +你是一个专注于 Rust 项目的架构审查专家。 + +你应该: +1. 重点关注模块解耦性和依赖方向 +2. 检查是否存在循环依赖 +3. 提出符合 Rust 惯用法的改进建议 +4. 关注错误处理和资源管理 +``` + +### 8.3 字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `description` | string | 是 | 简短描述,用于专家选择 | +| `name` | string | 否 | 专家名称,默认取目录名 | +| `allowed_skills` | array | 否 | 技能白名单,指定后仅这些技能可用;不指定则不限制 | +| `denied_skills` | array | 否 | 技能黑名单,指定后这些技能被禁用;在白名单之后应用 | +| `allowed_tools` | array | 否 | 工具白名单,指定后仅这些工具可用;不指定则不限制 | +| `denied_tools` | array | 否 | 工具黑名单,指定后这些工具被禁用;在白名单之后应用 | +| `allowed_subagents` | array | 否 | 子代理白名单,指定后仅允许创建这些类型的子代理;不指定则不限制 | +| `denied_subagents` | array | 否 | 子代理黑名单,指定后禁止创建这些类型的子代理;在白名单之后应用 | +| `provider` | string | 否 | 覆盖 provider,引用 `config.json` 的 `providers` 表中的键名;不指定则继承主智能体 | +| `model` | string | 否 | 覆盖 model,引用 `config.json` 的 `models` 表中的键名;不指定则继承主智能体 | + +> **能力过滤语义**:与子代理一致,`allowed_*` 与 `denied_*` 可共存,生效顺序为:先应用白名单(取交集),再扣除黑名单。四类能力(tools/skills/subagents)独立过滤,互不影响。全部不指定时,主智能体使用完整工具集和技能集。 +> +> **模型覆盖语义**:`provider` 和 `model` 字段独立可选,可只覆盖其一。引用不存在的 provider/model 名会阻止会话并明确反馈。不指定时继承主智能体的 provider/model 配置。 +> +> **专家与子代理的区别**:专家配置作用于主智能体(影响 system prompt 和能力策略),子代理配置作用于独立执行上下文(影响子任务的行为和能力)。两者各自独立,子代理不继承专家的能力策略。 + +### 8.4 专家管理 + +专家支持通过 Web UI 或 API 进行运行时管理: + +- 查看专家列表(含来源、禁用状态) +- 创建/更新/删除专家 +- 按作用域(用户级/项目级)禁用/启用专家 +- 配置能力策略(工具/技能/子代理黑白名单)和模型覆盖 + +专家启用/禁用状态写入独立状态文件: + +- 用户级状态:`~/.picobot/expert-state.json` +- 项目级状态:`.picobot/expert-state.json` + +## 9. 子代理系统 PicoBot 支持通过 `task` 工具创建子代理来处理复杂多步骤任务。子代理在一个独立的执行上下文中运行,拥有独立的会话历史和工具权限。 -### 8.1 内置子代理类型 +### 9.1 内置子代理类型 - **general**:通用型子代理,适合处理复杂多步骤任务。可以使用读写文件、执行命令、HTTP 请求等完整工具集。 > 如需只读探索型子代理,可通过自定义子代理配合 `allowed_tools` 白名单实现(见下文)。 -### 8.2 自定义子代理 +### 9.2 自定义子代理 用户可以在文件系统上定义新的子代理类型,类似于技能的加载方式。子代理定义文件采用 YAML frontmatter + body 的格式。 @@ -541,6 +613,12 @@ prompt_template: | 注意: 你是一个只读代理,禁止执行任何修改操作。 allowed_tools: [read, bash, web_fetch] # 可选,工具白名单(仅这些工具可用) denied_tools: [task] # 可选,工具黑名单(这些工具被禁用) +allowed_skills: [code-review-skill] # 可选,技能白名单(仅这些技能可用) +denied_skills: [shell-exec] # 可选,技能黑名单(这些技能被禁用) +allowed_subagents: [general] # 可选,子代理白名单(仅允许创建这些子代理类型) +denied_subagents: [explore] # 可选,子代理黑名单(禁止创建这些子代理类型) +provider: deepseek # 可选,覆盖 provider(引用 config.json 的 providers 表) +model: deepseek-chat # 可选,覆盖 model(引用 config.json 的 models 表) max_execution_secs: 600 # 可选,覆盖默认执行时间 --- @@ -559,9 +637,17 @@ max_execution_secs: 600 # 可选,覆盖默认执行时间 | `prompt_template` | string | 是 | 提示词模板,支持变量插值 | | `allowed_tools` | array | 否 | 工具白名单,指定后仅这些工具可用;不指定则不限制 | | `denied_tools` | array | 否 | 工具黑名单,指定后这些工具被禁用;在白名单之后应用 | +| `allowed_skills` | array | 否 | 技能白名单,指定后仅这些技能可用;不指定则不限制 | +| `denied_skills` | array | 否 | 技能黑名单,指定后这些技能被禁用;在白名单之后应用 | +| `allowed_subagents` | array | 否 | 子代理白名单,指定后仅允许创建这些类型的子代理;不指定则不限制 | +| `denied_subagents` | array | 否 | 子代理黑名单,指定后禁止创建这些类型的子代理;在白名单之后应用 | +| `provider` | string | 否 | 覆盖 provider,引用 `config.json` 的 `providers` 表中的键名;不指定则继承主智能体 | +| `model` | string | 否 | 覆盖 model,引用 `config.json` 的 `models` 表中的键名;不指定则继承主智能体 | | `max_execution_secs` | integer | 否 | 最大执行时间(秒) | -> **工具过滤语义**:`allowed_tools` 与 `denied_tools` 可共存。生效顺序为:先应用白名单(取交集),再扣除黑名单。两者都不指定时,子代理使用完整工具集。当子代理嵌套深度达到 `max_nesting_depth`(默认 2,即孙代理)时,始终移除 `task` 工具以防无限嵌套。白名单中未注册的工具名会被静默跳过。 +> **能力过滤语义**:`allowed_*` 与 `denied_*` 可共存。生效顺序为:先应用白名单(取交集),再扣除黑名单。四类能力(tools/skills/subagents)独立过滤,互不影响。全部不指定时,子代理使用完整工具集和技能集。当子代理嵌套深度达到 `max_nesting_depth` 时,始终移除 `task` 工具以防无限嵌套。白名单中未注册的工具/技能名会被静默跳过。 +> +> **模型覆盖语义**:`provider` 和 `model` 字段独立可选,可只覆盖其一。引用不存在的 provider/model 名会导致子代理创建失败并反馈给主智能体重试。不指定时继承主智能体的 provider/model 配置。 #### 模板变量 @@ -586,23 +672,34 @@ max_execution_secs: 600 # 可选,覆盖默认执行时间 如果指定的子代理类型不存在,系统会自动回退到 `general` 类型。 -### 8.3 子代理配置 +### 9.3 子代理配置 ```json { "subagents": { "enabled": true, "sources": ["user", "project"] + }, + "tools": { + "task": { + "enabled": true, + "max_nesting_depth": 2, + "max_execution_secs": 3600, + "ttl_hours": 24 + } } } ``` | 字段 | 默认值 | 说明 | |------|--------|------| -| `enabled` | `true` | 是否启用自定义子代理发现 | -| `sources` | `["user", "project"]` | 定义来源优先级 | +| `subagents.enabled` | `true` | 是否启用自定义子代理发现 | +| `subagents.sources` | `["user", "project"]` | 定义来源优先级 | +| `tools.task.max_nesting_depth` | `2` | 子代理最大嵌套深度(0=禁止嵌套,1=仅子代理,2=子代理+孙代理) | +| `tools.task.max_execution_secs` | `3600` | 子代理默认最大执行时间(秒) | +| `tools.task.ttl_hours` | `24` | 子代理任务 TTL(小时) | -## 9. 工具机制 +## 10. 工具机制 PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具包括: @@ -632,7 +729,7 @@ PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具 - bash / shell / http_request / web_fetch 让 Agent 具备更强的外部交互能力(bash 和 shell 是同一工具在不同平台的名称) - task 允许 Agent 创建独立上下文的子代理来处理复杂多步骤任务,支持内置类型(general)和用户自定义类型 -### 9.1 MCP 工具集成 +### 10.1 MCP 工具集成 PicoBot 支持通过 MCP (Model Context Protocol) 扩展工具能力,可以连接外部 MCP servers 并自动发现其提供的工具。配置格式兼容 Claude Desktop / Cursor。 @@ -709,18 +806,18 @@ MCP 工具会自动注册到 ToolRegistry,命名格式为 `mcp_{server_key}_{t - 通过 Tool trait 适配器接入,无需修改核心代码 - 连接失败不影响 Gateway 运行 -## 10. 调度器机制 +## 11. 调度器机制 PicoBot 带有一个基于 SQLite 的调度器,而不是纯内存或 JSON 文件驱动的任务系统。 -### 10.1 支持的调度类型 +### 11.1 支持的调度类型 - delay:延迟执行一次 - interval:固定间隔执行 - at:某个绝对时间执行一次 - cron:cron 表达式调度 -### 10.2 支持的任务类型 +### 11.2 支持的任务类型 - internal_event:内部事件 - outbound_message:直接向目标通道发消息 @@ -743,7 +840,7 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由 - 执行失败时会向主 chat 发送一条失败通知,便于用户感知异常 - 后台任务的历史、压缩和会话内上下文会留在独立会话中,不污染主会话 -### 10.3 运行时管理 +### 11.3 运行时管理 通过 scheduler_manage 可以进行: @@ -837,15 +934,15 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由 - agent_task:用户需要直接收到结果,例如日报提醒、定时播报、定时外发通知 - silent_agent_task:任务需要长期积累独立上下文或后台整理材料,但不应污染主会话,例如周报草稿整理、周期性资料汇总、后台分析任务 -## 11. 渠道与运行方式 +## 12. 渠道与运行方式 -### 11.1 当前支持的通道 +### 12.1 当前支持的通道 - WebSocket CLI 客户端 - 飞书通道 - 微信通道 -### 11.2 Gateway 接口 +### 12.2 Gateway 接口 网关当前暴露: @@ -853,7 +950,7 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由 - /ws:CLI 客户端连接入口 - /:Web UI 前端(已嵌入二进制,无需外部文件) -### 11.3 Web UI +### 12.3 Web UI PicoBot 内置 Web 前端,在编译时已打包进二进制文件。启动网关后可直接访问: @@ -866,7 +963,7 @@ http://127.0.0.1:19876/ - 支持 SPA 前端路由 - 开发模式下可通过 `STATIC_DIR` 环境变量使用磁盘文件(支持热更新) -### 11.4 CLI 使用方式 +### 12.4 CLI 使用方式 程序提供两个主命令: @@ -893,7 +990,7 @@ CLI 中已实现的交互命令包括: - /clear - /quit -## 12. 配置说明 +## 13. 配置说明 配置默认从以下位置加载: @@ -960,9 +1057,9 @@ CLI 中已实现的交互命令包括: - tools:工具启用/禁用配置(通过 disabled 列表指定禁用的工具) - time.timezone:时区,默认应使用 IANA 时区名,例如 Asia/Shanghai -## 13. 快速开始 +## 14. 快速开始 -### 13.1 准备配置 +### 14.1 准备配置 1. 复制并修改 config.json,或把配置放到 ~/.picobot/config.json 2. 配置好 Provider 的 base_url、api_key、model_id @@ -1008,7 +1105,7 @@ CLI 中已实现的交互命令包括: } ``` -### 13.2 构建与启动 +### 14.2 构建与启动 > **Windows 用户注意:** Windows 默认不包含 `make` 命令。你可以通过以下方式之一来构建: > @@ -1081,7 +1178,7 @@ $env:STATIC_DIR="static"; cargo run -- gateway | `make run` | `cargo run --release -- gateway` | `cargo run --release -- gateway` | | `make check` | `cd web && npm run build && cargo check && cargo clippy` | `cd web; npm run build; cd ..; cargo check; cargo clippy` | -### 13.3 启动本地 CLI +### 14.3 启动本地 CLI ```bash cargo run -- agent @@ -1099,13 +1196,13 @@ ws://127.0.0.1:19876/ws cargo run -- agent --gateway-url ws://127.0.0.1:19876/ws ``` -### 13.4 检查服务状态 +### 14.4 检查服务状态 ```bash curl http://127.0.0.1:19876/health ``` -## 14. 目录结构 +## 15. 目录结构 ```text PicoBot/ @@ -1117,7 +1214,8 @@ PicoBot/ │ ├── client/ # WebSocket CLI 客户端 │ ├── command/ # 命令系统(处理器、适配器、上下文) │ ├── config/ # 配置解析 -│ ├── domain/ # 领域模型(消息、工具定义) +│ ├── domain/ # 领域模型(消息、工具定义、能力策略) +│ ├── experts/ # 专家系统(发现、加载、运行时管理) │ ├── gateway/ # Gateway、Session 编排、WS/HTTP 控制面、执行服务 │ ├── logging/ # 日志配置 │ ├── mcp/ # MCP 集成(客户端管理、工具适配器、配置) @@ -1136,7 +1234,7 @@ PicoBot/ └── config.json ``` -## 15. 测试与维护建议 +## 16. 测试与维护建议 当前 tests 目录中已经包含 Provider 集成测试和工具调用相关测试,但部分测试依赖外部 API Key,需要先准备 tests/test.env。 @@ -1150,13 +1248,14 @@ PicoBot/ - src/bus/message.rs:消息结构变更(如 OutboundMessage 新增 session_id) - src/command/handlers/:命令处理器实现 -## 16. 总结 +## 17. 总结 PicoBot 当前已经具备一个可长期运行 Agent 系统的关键组件: - 有入口:Gateway + Channel - 有状态:SQLite + Session 恢复 - 有能力:工具调用 + 技能系统 + MCP 扩展 + 可自定义子代理 +- 有角色:专家系统 + 能力策略(工具/技能/子代理黑白名单)+ 模型独立覆盖 - 有记忆:长期记忆 + 自动维护摘要 - 有计划:Scheduler + agent_task diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 8f195cb..005c14b 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -7,6 +7,7 @@ export const API = { skills: '/api/skills', skillsToggle: '/api/skills/toggle', tools: '/api/tools', + modelOptions: '/api/model-options', subagents: '/api/subagents', subagentsToggle: '/api/subagents/toggle', subagentsUpdate: '/api/subagents/update', diff --git a/web/src/api/experts.ts b/web/src/api/experts.ts index 2ba08cd..a2b3474 100644 --- a/web/src/api/experts.ts +++ b/web/src/api/experts.ts @@ -1,10 +1,14 @@ import { API, apiGetSilent } from './client' -import type { ExpertListResponse, ExpertItem, CapabilityPolicy } from '../components/Settings/types' +import type { ExpertListResponse, ExpertItem, CapabilityPolicy, ModelOptionsResponse } from '../components/Settings/types' export function listExperts(): Promise { return apiGetSilent(API.experts) } +export function listModelOptions(): Promise { + return apiGetSilent(API.modelOptions) +} + export async function toggleExpert(name: string, scope: string, enabled: boolean): Promise { return fetch(API.expertsToggle, { method: 'POST', @@ -13,7 +17,7 @@ export async function toggleExpert(name: string, scope: string, enabled: boolean }) } -export async function createExpert(payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy }): Promise { +export async function createExpert(payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy; provider?: string; model?: string }): Promise { return fetch(API.expertsCreate, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -21,7 +25,7 @@ export async function createExpert(payload: { name: string; description: string; }) } -export async function updateExpert(payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy }): Promise { +export async function updateExpert(payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy; provider?: string; model?: string }): Promise { return fetch(API.expertsUpdate, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, diff --git a/web/src/api/subagents.ts b/web/src/api/subagents.ts index 6eb880a..4d3c9eb 100644 --- a/web/src/api/subagents.ts +++ b/web/src/api/subagents.ts @@ -18,6 +18,8 @@ export async function updateSubagent(payload: { description?: string body?: string capability?: CapabilityPolicy + provider?: string + model?: string }): Promise { return fetch(API.subagentsUpdate, { method: 'PUT', diff --git a/web/src/components/Settings/ConfigPage.tsx b/web/src/components/Settings/ConfigPage.tsx index 19d7070..7a5c970 100644 --- a/web/src/components/Settings/ConfigPage.tsx +++ b/web/src/components/Settings/ConfigPage.tsx @@ -16,6 +16,7 @@ import type { CapabilityPolicy, KnownSource, SchedulerConfig, ChannelConfig, + ModelOptionsResponse, } from './types' import { TABS, inputCls, selectCls, TIMEZONE_OPTIONS } from './constants' import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader, CheckboxList, ModalHeader, ModalFooter } from './ui' @@ -23,7 +24,7 @@ import { getAppConfig, updateAppConfig, restartGateway, checkHealth } from '../. import { listSkills, toggleSkill } from '../../api/skills' import { listTools } from '../../api/tools' import { listSubagents, toggleSubagent, updateSubagent } from '../../api/subagents' -import { listExperts, toggleExpert, createExpert, updateExpert, deleteExpert } from '../../api/experts' +import { listExperts, toggleExpert, createExpert, updateExpert, deleteExpert, listModelOptions } from '../../api/experts' import { getMcpStatus } from '../../api/mcp' export { getSelectedExpert, selectExpert } from '../../api/experts' @@ -56,6 +57,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage const [subagentListLoading, setSubagentListLoading] = useState(false) const [expertList, setExpertList] = useState(null) const [expertListLoading, setExpertListLoading] = useState(false) + const [modelOptions, setModelOptions] = useState(null) const [editingExpert, setEditingExpert] = useState<{ mode: 'create' | 'edit' name?: string @@ -63,6 +65,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage nameField: string description: string body: string + provider: string + model: string allowedSkills: string[] deniedSkills: string[] allowedTools: string[] @@ -75,6 +79,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage const [editingSubagent, setEditingSubagent] = useState<{ name: string description: string + provider: string + model: string allowedSkills: string[] deniedSkills: string[] allowedTools: string[] @@ -119,7 +125,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage return toggleSubagent(name, scope, enabled) }, []) - const updateSubagentCb = useCallback(async (payload: { name: string; description?: string; body?: string; capability?: CapabilityPolicy }) => { + const updateSubagentCb = useCallback(async (payload: { name: string; description?: string; body?: string; capability?: CapabilityPolicy; provider?: string; model?: string }) => { return updateSubagent(payload) }, []) @@ -134,11 +140,11 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage return toggleExpert(name, scope, enabled) }, []) - const createExpertCb = useCallback(async (payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy }) => { + const createExpertCb = useCallback(async (payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy; provider?: string; model?: string }) => { return createExpert(payload) }, []) - const updateExpertCb = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy }) => { + const updateExpertCb = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy; provider?: string; model?: string }) => { return updateExpert(payload) }, []) @@ -175,13 +181,14 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage if (activeTab === 'subagents') fetchSubagentList() }, [activeTab, fetchSubagentList]) - // Fetch skills + tools when experts/subagents tab is selected (for capability CheckboxList) + // Fetch skills + tools + model options when experts/subagents tab is selected (for capability CheckboxList & provider/model dropdowns) useEffect(() => { if (activeTab === 'experts' || activeTab === 'subagents') { if (!skillList) fetchSkillList() if (!toolList) fetchToolList() + if (!modelOptions) listModelOptions().then(data => { if (data) setModelOptions(data) }) } - }, [activeTab, fetchSkillList, fetchToolList, skillList, toolList]) + }, [activeTab, fetchSkillList, fetchToolList, skillList, toolList, modelOptions]) // experts tab 编辑专家时也需要子代理勾选列表,按需加载(subagents tab 由下方独立 useEffect 刷新) useEffect(() => { @@ -707,6 +714,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage setEditingSubagent({ name: subagent.name, description: subagent.description, + provider: subagent.provider ?? '', + model: subagent.model ?? '', allowedSkills: subagent.capability?.allowed_skills ?? [], deniedSkills: subagent.capability?.denied_skills ?? [], allowedTools: subagent.capability?.allowed_tools ?? [], @@ -781,7 +790,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage