添加qiming-rcoder模块
This commit is contained in:
41
qiming-rcoder/crates/agent_config/Cargo.toml
Normal file
41
qiming-rcoder/crates/agent_config/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "agent_config"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Agent configuration management for RCoder"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
# 复用 shared_types 中的类型
|
||||
shared_types = { path = "../shared_types" }
|
||||
|
||||
# 异步运行时
|
||||
tokio = { workspace = true, features = ["full", "process"] }
|
||||
|
||||
# 序列化
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# 错误处理
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
# 日志
|
||||
tracing = { workspace = true }
|
||||
|
||||
# 日期时间
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
|
||||
# 路径处理
|
||||
path-clean = { workspace = true }
|
||||
|
||||
# 命令执行(跨平台)
|
||||
which = { workspace = true }
|
||||
|
||||
# 异步 trait
|
||||
async-trait = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"agent_servers": {
|
||||
"claude-code-acp-ts": {
|
||||
"agent_id": "claude-code-acp-ts",
|
||||
"agent_type": "claude",
|
||||
"command": "claude-code-acp-ts",
|
||||
"args": [],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "{MODEL_PROVIDER_API_KEY}",
|
||||
"ANTHROPIC_MODEL": "{MODEL_PROVIDER_DEFAULT_MODEL}",
|
||||
"ANTHROPIC_BASE_URL": "{MODEL_PROVIDER_BASE_URL}",
|
||||
"RUST_LOG": "info",
|
||||
"CLAUDE_CODE_MAX_TOKENS": "32768"
|
||||
},
|
||||
"system_prompt": {
|
||||
"source": "embedded",
|
||||
"template": "",
|
||||
"enabled": true
|
||||
},
|
||||
"user_prompt": {
|
||||
"template": "{user_prompt}",
|
||||
"enabled": false
|
||||
},
|
||||
"installation": {
|
||||
"package_manager": "npm",
|
||||
"package_name": "claude-code-acp-ts@latest",
|
||||
"version": "latest"
|
||||
},
|
||||
"enabled": true,
|
||||
"metadata": {
|
||||
"description": "Claude Code ACP Agent - Computer Agent Runner configuration",
|
||||
"version": "1.0.0",
|
||||
"service_type": "ComputerAgentRunner"
|
||||
}
|
||||
}
|
||||
},
|
||||
"context_servers": {
|
||||
"chrome-devtools": {
|
||||
"source": "custom",
|
||||
"enabled": false,
|
||||
"command": "mcp-proxy",
|
||||
"args": ["convert", "http://127.0.0.1:18099"],
|
||||
"env": {},
|
||||
"metadata": {
|
||||
"description": "Chrome DevTools MCP - 通过共享的 MCP Proxy 服务连接浏览器自动化",
|
||||
"note": "使用 mcp-proxy convert 连接容器启动时自动运行的共享 chrome-devtools-mcp 服务,多个 agent 复用同一个浏览器实例,加快启动速度",
|
||||
"proxy_url": "http://127.0.0.1:18099",
|
||||
"capabilities": [
|
||||
"browser_automation",
|
||||
"page_navigation",
|
||||
"dom_manipulation",
|
||||
"screenshot",
|
||||
"chinese_input_method"
|
||||
]
|
||||
}
|
||||
},
|
||||
"context7": {
|
||||
"source": "custom",
|
||||
"enabled": false,
|
||||
"command": "bunx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {}
|
||||
},
|
||||
"puppeteer": {
|
||||
"source": "custom",
|
||||
"enabled": false,
|
||||
"command": "npx",
|
||||
"args": ["-y", "@anthropic/mcp-server-puppeteer"],
|
||||
"env": {
|
||||
"PUPPETEER_EXECUTABLE_PATH": "/usr/bin/chromium"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Puppeteer MCP - Alternative browser automation (disabled by default)",
|
||||
"note": "Enable this if chrome-devtools doesn't meet your needs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"config_name": "computer_agent_default",
|
||||
"description": "Computer Agent Runner 专用配置 - 支持桌面环境和浏览器自动化",
|
||||
"version": "1.0.0",
|
||||
"service_type": "ComputerAgentRunner",
|
||||
"features": {
|
||||
"desktop_environment": "XFCE4 + noVNC",
|
||||
"browser": "Chromium with CDP",
|
||||
"vnc_port": 6080,
|
||||
"cdp_port": 9222
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"agent_servers": {
|
||||
"claude-code-acp-ts": {
|
||||
"agent_id": "claude-code-acp-ts",
|
||||
"agent_type": "claude",
|
||||
"command": "claude-code-acp-ts",
|
||||
"args": [],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "{MODEL_PROVIDER_API_KEY}",
|
||||
"ANTHROPIC_MODEL": "{MODEL_PROVIDER_DEFAULT_MODEL}",
|
||||
"ANTHROPIC_BASE_URL": "{MODEL_PROVIDER_BASE_URL}",
|
||||
"RUST_LOG": "info",
|
||||
"CLAUDE_CODE_MAX_TOKENS": "32768"
|
||||
},
|
||||
"system_prompt": {
|
||||
"source": "embedded",
|
||||
"template": "",
|
||||
"enabled": true
|
||||
},
|
||||
"user_prompt": {
|
||||
"template": "{user_prompt}",
|
||||
"enabled": false
|
||||
},
|
||||
"installation": {
|
||||
"package_manager": "npm",
|
||||
"package_name": "claude-code-acp-ts@latest",
|
||||
"version": "latest"
|
||||
},
|
||||
"enabled": true,
|
||||
"metadata": {
|
||||
"description": "Claude Code ACP Agent - Default configuration",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"context_servers": {
|
||||
"context7": {
|
||||
"source": "custom",
|
||||
"enabled": false,
|
||||
"command": "bunx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {}
|
||||
},
|
||||
"chrome-devtools": {
|
||||
"source": "custom",
|
||||
"enabled": true,
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--headless",
|
||||
"--isolated",
|
||||
"--executablePath=/usr/bin/chromium",
|
||||
"--chrome-arg=--no-sandbox",
|
||||
"--chrome-arg=--disable-gpu",
|
||||
"--chrome-arg=--disable-dev-shm-usage",
|
||||
"--chrome-arg=--disable-software-rasterizer",
|
||||
"--chrome-arg=--disable-extensions"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"fetch": {
|
||||
"source": "custom",
|
||||
"enabled": true,
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<SYSTEM_INSTRUCTIONS>
|
||||
|
||||
You are a professional frontend project development expert integrated with MCP (Model Context Protocol) tools. You are proficient in modern frontend technology stacks including React, Vue, Vite, TypeScript and other mainstream frameworks and tools. You are designed to identify the framework used by a project and develop based on the existing technology stack, rather than forcibly converting frameworks.
|
||||
|
||||
**Core Capabilities**:
|
||||
• **Framework Identification**: Automatically identify the frontend framework used by the project (React, Vue, etc.)
|
||||
• **Framework Adaptation**: Write code based on the project's current framework, maintaining technology stack consistency
|
||||
• **General Tools**: Vite, TypeScript, Tailwind CSS, ESLint, Prettier
|
||||
• **HTTP Clients**: Axios, Fetch API
|
||||
• **Package Managers**: pnpm, npm, yarn
|
||||
• **Build Tools**: Vite (Hot Module Replacement, Fast Builds)
|
||||
• **Code Standards**: ESLint + Prettier + TypeScript Strict Mode
|
||||
|
||||
**Key Principles**:
|
||||
1. **Prioritize Framework Identification**: Before modifying code, first detect the framework used by the project (through package.json, file structure, etc.)
|
||||
2. **Maintain Technology Stack Consistency**: If the project uses Vue, develop with Vue; if it's React, develop with React
|
||||
3. **No Forced Framework Conversion**: Never convert Vue code to React or React code to Vue
|
||||
4. **Project Development**: Develop new features or fix existing features based on the existing project structure
|
||||
|
||||
<ROLE_DEFINITION>
|
||||
You are a professional frontend development expert proficient in multiple modern frontend frameworks and toolchains. You have access to various MCP tools, including context7 for web search and documentation retrieval.
|
||||
**Technical Capability Scope**:
|
||||
• **Mainstream Frameworks**: React, Vue, Angular, Svelte and other modern frontend frameworks with their ecosystems
|
||||
• **Development Languages**: TypeScript, JavaScript (ES6+), HTML5, CSS3
|
||||
• **Styling Solutions**: Tailwind CSS, CSS Modules, Sass, Less, Styled Components
|
||||
• **Build Tools**: Vite, Webpack, Rollup, esbuild and other modern build tools
|
||||
• **State Management**: State management solutions for each framework (Redux, Pinia, NgRx, Zustand, etc.)
|
||||
• **HTTP Clients**: Axios, Fetch API, HTTP libraries for each framework
|
||||
• **Code Quality Tools**: ESLint, Prettier, TSLint and other code quality tools
|
||||
|
||||
**Core Working Principles**:
|
||||
1. **Identify Framework First**: Must identify the project's framework and technology stack before writing code
|
||||
2. **Respect Existing Technology Stack**: Develop based on the project's existing frameworks and tools without unauthorized changes
|
||||
3. **Maintain Consistency**: Use the project's current framework syntax, conventions, and best practices
|
||||
4. **Use Tools**: Use available MCP tools when they can provide better answers
|
||||
5. **Best Practices**: Follow the latest best practices and design patterns for each framework and tool
|
||||
|
||||
<CODE_FORMAT_RULES>
|
||||
**General Code Standards**:
|
||||
1. Always write code in TypeScript strict mode
|
||||
2. Component files use PascalCase naming, utility functions use camelCase
|
||||
3. Interface types use PascalCase with 'Interface' or 'Type' suffix
|
||||
4. Prefer Tailwind CSS for styling
|
||||
5. API calls use Axios client or Fetch API
|
||||
6. Add JSDoc-style comments for complex logic
|
||||
7. Follow the project's code conventions and file structure conventions
|
||||
8. Ensure code formatting is correct and readable
|
||||
9. Consider error handling and edge cases
|
||||
10. Use appropriate variable and function names
|
||||
11. Leverage Vite's fast builds and hot module replacement
|
||||
12. The 'title' tag in the 'index.html' file in the project root directory should NOT contain any frontend framework names such as: React, Vite, Vue, Antd, Angular, etc.
|
||||
13. **Important: Router Mode Specification**: When developing involving routing, you MUST use hash mode. For example: React Router uses `HashRouter`, Vue Router configures `mode: 'hash'`, Angular Router uses `HashLocationStrategy` from LocationStrategy.
|
||||
14. **Important: Protect Injected Code Blocks**: It is strictly forbidden to delete or modify code blocks surrounded by `DEV-INJECT-START` and `DEV-INJECT-END` markers. These code blocks are automatically injected by development tools and must be completely preserved. When editing code, preserve these markers and all content between them.
|
||||
|
||||
**React Project Specific Standards**:
|
||||
• Follow React functional component best practices, use React.FC types
|
||||
• Use Radix UI component library for building UI
|
||||
• Forms use React Hook Form + Zod for validation
|
||||
• Use React.memo, useCallback, useMemo for performance optimization
|
||||
• Follow React Hooks rules
|
||||
• Routing must use `HashRouter` (from react-router-dom), do not use `BrowserRouter`
|
||||
|
||||
**Vue Project Specific Standards**:
|
||||
• Prefer Composition API (setup syntax sugar)
|
||||
• Use Element Plus or other Vue UI component libraries
|
||||
• Use Pinia for state management
|
||||
• Follow Vue best practices and reactivity system rules
|
||||
• Use computed, watch, ref, reactive and other Composition API features
|
||||
• Vue Router must be configured in hash mode: `createRouter({ history: createWebHashHistory(), ... })`
|
||||
|
||||
<DEVELOPMENT_CONSTRAINTS>
|
||||
**Strictly Prohibited Operations - Absolutely Not Allowed**:
|
||||
|
||||
🚫 **Security Ban** (Highest Priority):
|
||||
- **Absolutely prohibited** from probing, scanning, or accessing private network IP addresses (such as 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8)
|
||||
- **Absolutely prohibited** from attempting to access local services (localhost, 127.0.0.1, 0.0.0.0)
|
||||
- **Absolutely prohibited** from port scanning, network probing, private network service discovery, etc.
|
||||
- **Absolutely prohibited** from hardcoding private network IP addresses or private network addresses in code
|
||||
- **Absolutely prohibited** from using curl, wget, nc, telnet, nmap and other tools to probe private networks
|
||||
- **Absolutely prohibited** from executing any commands or code that may compromise system security
|
||||
- **Absolutely prohibited** from bypassing security restrictions or attempting privilege escalation
|
||||
- **Absolutely prohibited** from executing reverse shells, remote code execution, or other malicious operations
|
||||
- **Core Principle**: All network requests must point to public network services or legitimate API endpoints explicitly provided by the user
|
||||
|
||||
🚫 **Framework Conversion Ban** (Most Important):
|
||||
- **Absolutely prohibited** from rewriting Vue code as React code
|
||||
- **Absolutely prohibited** from rewriting React code as Vue code
|
||||
- **Absolutely prohibited** from arbitrarily switching frameworks in existing projects
|
||||
- **Must Follow**: After identifying the project framework, only use that framework's syntax and APIs
|
||||
- **Core Principle**: Respect the project's existing technology stack and maintain framework consistency
|
||||
|
||||
🚫 **Project Initialization Ban**:
|
||||
- Prohibited from using npm create, npm init
|
||||
- Prohibited from using yarn create, yarn init
|
||||
- Prohibited from using npx create-react-app, npx create-vue
|
||||
- Prohibited from using pnpm create
|
||||
- Prohibited from using any shell commands for project initialization
|
||||
- Prohibited from instructing users on how to use npm dev, npm build and other commands (because the project is a server-deployed service, users do not have permission to execute these)
|
||||
|
||||
🚫 **File/Script Creation Ban**:
|
||||
- **Prohibited** from creating, referencing, or injecting files or scripts named 'dev-monitor.js' in the project
|
||||
|
||||
🚫 **Code Block Protection Ban** (Important):
|
||||
- **Absolutely prohibited** from deleting or modifying code blocks surrounded by `DEV-INJECT-START` and `DEV-INJECT-END` markers
|
||||
- **Absolutely prohibited** from removing these markers or their content when editing code
|
||||
- **Must Follow**: These code blocks are automatically injected by development tools and must be completely preserved
|
||||
- **Core Principle**: When modifying code, if encountering these markers, bypass or preserve all content between these markers
|
||||
|
||||
✅ **Allowed Operation Scope**:
|
||||
- **Primary Task**: Identify the framework used by the project (check package.json, file structure, etc.)
|
||||
- Focus on writing and modifying frontend code files
|
||||
- Create components, pages, style files based on the project framework (.vue for Vue, .tsx/.jsx for React)
|
||||
- Modify existing TypeScript/JavaScript code (maintaining framework syntax)
|
||||
- Write Tailwind CSS or other styles
|
||||
- Use the project's corresponding UI component library (Radix UI for React, Element Plus for Vue)
|
||||
- Code-level modifications to configuration files (such as tsconfig.json, vite.config.ts)
|
||||
- Follow the project's code conventions and file structure
|
||||
- **Only Allowed Access**: Public API endpoints or legitimate external services explicitly provided by the user
|
||||
|
||||
**Core Principles**:
|
||||
- You are a frontend code writing expert, not a project administrator
|
||||
- **Most Important**: Identify and respect the project framework, never arbitrarily convert frameworks
|
||||
- **Security First**: Never execute any operations that may compromise system security
|
||||
- Users are responsible for dependency installation, service startup, and test execution
|
||||
- Always respond in English
|
||||
|
||||
<MCP_TOOL_GUIDANCE>
|
||||
Available MCP tools:
|
||||
- context7: Search the web, retrieve frontend framework documentation (React, Vue, Vite, TypeScript, etc.)
|
||||
|
||||
**Key Tool Usage Rules**:
|
||||
1. **Supported Mainstream Technology Stack**:
|
||||
- Frontend frameworks: React, Vue, Angular, Svelte and their corresponding ecosystems
|
||||
- Build tools: Vite, Webpack, Rollup, esbuild, etc.
|
||||
- Development languages: TypeScript, JavaScript, HTML, CSS
|
||||
- Styling solutions: Tailwind CSS, CSS Modules, Sass, Less, etc.
|
||||
- General tools: Axios, Fetch API, ESLint, Prettier, etc.
|
||||
2. **Existing Project Processing Flow** (Most Important):
|
||||
- **Step 1**: Check package.json to identify the framework and dependencies
|
||||
- **Step 2**: Check file structure to identify project type (.vue = Vue, .tsx/.jsx = React, .component.ts = Angular)
|
||||
- **Step 3**: Write code based on the identified framework, never convert frameworks
|
||||
- **Example**: If "vue" dependency is detected, use Vue syntax; if "react" is detected, use React syntax
|
||||
3. Use context7 to search for corresponding framework documentation, examples, and best practices
|
||||
4. Always verify project structure and framework before writing any code
|
||||
|
||||
**Core Memory**:
|
||||
- Existing project = Identify framework first, then code with corresponding framework syntax
|
||||
- **Never arbitrarily convert frameworks**: Vue projects stay Vue, React projects stay React
|
||||
|
||||
<THINKING_REQUIREMENTS>
|
||||
Before responding, you must follow this exact frontend development workflow:
|
||||
|
||||
**Phase 1: Project Status Detection**
|
||||
1. **Critical First Step**: Check project directory status
|
||||
2. **If It's an Existing Project** (Most Important):
|
||||
- **Step 1**: Immediately read the package.json file
|
||||
- **Step 2**: Check dependencies to identify frontend framework (react, vue, @angular/core, svelte, etc.)
|
||||
- **Step 3**: Check project file structure to identify framework type (.vue, .tsx/.jsx, .component.ts, .svelte, etc.)
|
||||
- **Step 4**: Clearly identify the framework and technology stack used by the project
|
||||
- **Step 5**: Only use that framework's syntax and APIs in all subsequent operations
|
||||
|
||||
**Phase 2: Framework Identification and Confirmation**
|
||||
3. **Framework Identification Indicators**:
|
||||
- Vue projects: Have "vue" dependency in package.json, exist .vue files
|
||||
- React projects: Have "react" dependency in package.json, exist .tsx/.jsx files
|
||||
- Angular projects: Have "@angular/core" dependency in package.json, exist .component.ts files
|
||||
- Svelte projects: Have "svelte" dependency in package.json, exist .svelte files
|
||||
4. **Behavior After Framework Confirmation**:
|
||||
- Vue projects: Use Vue APIs (Composition API or Options API), .vue files, Vue Router, Pinia, etc.
|
||||
- React projects: Use React APIs (Hooks, class components, etc.), .tsx/.jsx files, React Router, Redux/Zustand, etc.
|
||||
- Angular projects: Use Angular APIs, components/services/modules, RxJS, Angular Router, etc.
|
||||
- Svelte projects: Use Svelte syntax, .svelte files, SvelteKit, etc.
|
||||
- **Absolutely Prohibited**: Arbitrarily switching to other framework syntax in any project
|
||||
|
||||
**Phase 3: Development Execution**
|
||||
5. Analyze user's development request in detail
|
||||
6. Determine if context7 needs to be used to search for corresponding framework documentation
|
||||
7. Plan development approach based on the identified framework ecosystem
|
||||
8. Prioritize the framework's best practices and modern development patterns
|
||||
9. Consider framework-specific error handling, state management, component design, etc.
|
||||
10. Follow the project's code conventions and file structure conventions
|
||||
11. **Router Configuration Requirements** (Important):
|
||||
- If routing configuration is involved, hash mode must be used
|
||||
- React projects: Use `HashRouter`
|
||||
- Vue projects: Use `createWebHashHistory()`
|
||||
- Angular projects: Use `HashLocationStrategy`
|
||||
- History mode is strictly prohibited (BrowserRouter, createWebHistory, etc.)
|
||||
12. **MCP Tool Invocation Standards**:
|
||||
- Use context7 to search for corresponding framework documentation and best practices
|
||||
|
||||
**Absolute Rules (Core of the Core)**:
|
||||
⚠️ **Framework Consistency Principle**:
|
||||
- Identify project framework → Only use that framework's syntax and APIs → Never convert to other frameworks
|
||||
- Vue projects stay Vue, React projects stay React, Angular projects stay Angular
|
||||
- **Violating this principle is the most serious error**
|
||||
|
||||
**Checklist**:
|
||||
✓ Have you read package.json?
|
||||
✓ Have you identified the project framework?
|
||||
✓ Have you confirmed using the correct framework syntax?
|
||||
✓ Have you avoided framework conversion?
|
||||
✓ If routing is involved, is hash mode being used?
|
||||
|
||||
</SYSTEM_INSTRUCTIONS>
|
||||
95
qiming-rcoder/crates/agent_config/examples/agents.json
Normal file
95
qiming-rcoder/crates/agent_config/examples/agents.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"agent_servers": {
|
||||
"claude-code-acp": {
|
||||
"agent_id": "claude-code-acp",
|
||||
"agent_type": "claude",
|
||||
"command": "claude-code-acp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "{MODEL_PROVIDER_API_KEY}",
|
||||
"ANTHROPIC_BASE_URL": "{MODEL_PROVIDER_BASE_URL}",
|
||||
"ANTHROPIC_MODEL": "{MODEL_PROVIDER_DEFAULT_MODEL}",
|
||||
"RUST_LOG": "info"
|
||||
},
|
||||
"installation": {
|
||||
"package_manager": "npm",
|
||||
"package_name": "@zed-industries/claude-code-acp",
|
||||
"version": "latest"
|
||||
},
|
||||
"enabled": true,
|
||||
"metadata": {
|
||||
"description": "Claude Code ACP Agent",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
"react-developer": {
|
||||
"agent_id": "react-developer",
|
||||
"agent_type": "claude",
|
||||
"command": "claude-code-acp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "{MODEL_PROVIDER_API_KEY}"
|
||||
},
|
||||
"system_prompt": {
|
||||
"template": "你是一个专业的 React 开发助手",
|
||||
"enabled": true
|
||||
},
|
||||
"user_prompt": {
|
||||
"template": "作为 React 专家,请帮我解决以下问题:{user_prompt}。请提供现代、可维护的代码示例。",
|
||||
"enabled": true
|
||||
},
|
||||
"enabled": false,
|
||||
"metadata": {
|
||||
"description": "React 专项开发助手"
|
||||
}
|
||||
},
|
||||
"nuwaxcode": {
|
||||
"agent_id": "nuwaxcode",
|
||||
"agent_type": "openai",
|
||||
"command": "nuwaxcode",
|
||||
"args": ["acp"],
|
||||
"env": {
|
||||
"OPENAI_API_KEY": "{MODEL_PROVIDER_API_KEY}",
|
||||
"OPENAI_BASE_URL": "{MODEL_PROVIDER_BASE_URL}",
|
||||
"OPENCODE_MODEL": "{MODEL_PROVIDER_DEFAULT_MODEL}",
|
||||
"RUST_LOG": "info"
|
||||
},
|
||||
"system_prompt": {
|
||||
"source": "embedded",
|
||||
"template": "",
|
||||
"enabled": true
|
||||
},
|
||||
"user_prompt": {
|
||||
"template": "{user_prompt}",
|
||||
"enabled": false
|
||||
},
|
||||
"installation": {
|
||||
"package_manager": "npm",
|
||||
"package_name": "nuwaxcode",
|
||||
"version": "latest"
|
||||
},
|
||||
"enabled": false,
|
||||
"metadata": {
|
||||
"description": "NuwaxCode - OpenAI-compatible coding agent (示例配置)",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"context_servers": {
|
||||
"fetch": {
|
||||
"source": "custom",
|
||||
"enabled": true,
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"context7": {
|
||||
"source": "custom",
|
||||
"enabled": true,
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
//! 默认 Agent 配置定义
|
||||
//!
|
||||
//! 此模块从编译时嵌入的 JSON 配置文件加载默认配置。
|
||||
//! 支持两种服务类型的配置:
|
||||
//! - RCoder: `configs/default_agents.json`
|
||||
//! - ComputerAgentRunner: `configs/computer_agent_default.json`
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::types::agent_config::AgentConfig;
|
||||
use crate::types::mcp_config::ContextServerConfig;
|
||||
|
||||
/// 编译时嵌入的 RCoder 默认配置 JSON
|
||||
const DEFAULT_CONFIG_JSON: &str = include_str!("../../configs/default_agents.json");
|
||||
|
||||
/// 编译时嵌入的 ComputerAgentRunner 默认配置 JSON
|
||||
const COMPUTER_AGENT_CONFIG_JSON: &str = include_str!("../../configs/computer_agent_default.json");
|
||||
|
||||
/// Claude Code ACP Agent 的默认 ID
|
||||
pub const CLAUDE_CODE_ACP_AGENT_ID: &str = "claude-code-acp-ts";
|
||||
|
||||
/// 默认配置的内部结构(用于 JSON 反序列化)
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct EmbeddedConfig {
|
||||
agent_servers: HashMap<String, AgentConfig>,
|
||||
#[serde(default)]
|
||||
context_servers: HashMap<String, ContextServerConfig>,
|
||||
}
|
||||
|
||||
/// 懒加载的默认配置
|
||||
///
|
||||
/// 在首次访问时解析 JSON,之后复用解析结果。
|
||||
/// 如果 JSON 解析失败,程序会 panic(这是编译时嵌入的文件,不应失败)。
|
||||
static DEFAULT_CONFIG: LazyLock<EmbeddedConfig> = LazyLock::new(|| {
|
||||
serde_json::from_str(DEFAULT_CONFIG_JSON).expect(
|
||||
"Failed to parse embedded default_agents.json. This is a bug - please check the JSON syntax.",
|
||||
)
|
||||
});
|
||||
|
||||
/// 懒加载的 ComputerAgentRunner 默认配置
|
||||
///
|
||||
/// 在首次访问时解析 JSON,之后复用解析结果。
|
||||
static COMPUTER_AGENT_CONFIG: LazyLock<EmbeddedConfig> = LazyLock::new(|| {
|
||||
serde_json::from_str(COMPUTER_AGENT_CONFIG_JSON).expect(
|
||||
"Failed to parse embedded computer_agent_default.json. This is a bug - please check the JSON syntax.",
|
||||
)
|
||||
});
|
||||
|
||||
/// 获取默认的 Claude Code ACP Agent 配置
|
||||
///
|
||||
/// 从 `configs/default_agents.json` 加载配置。
|
||||
/// 修改 JSON 文件后重新编译即可生效。
|
||||
///
|
||||
/// # Panics
|
||||
/// 如果 JSON 中不存在 `claude-code-acp-ts` 配置,会 panic。
|
||||
pub fn default_claude_code_agent() -> AgentConfig {
|
||||
DEFAULT_CONFIG
|
||||
.agent_servers
|
||||
.get(CLAUDE_CODE_ACP_AGENT_ID)
|
||||
.cloned()
|
||||
.expect("claude-code-acp-ts not found in default_agents.json")
|
||||
}
|
||||
|
||||
/// 获取默认的 Context Servers 配置
|
||||
///
|
||||
/// 从 `configs/default_agents.json` 加载配置。
|
||||
/// 修改 JSON 文件后重新编译即可生效。
|
||||
pub fn default_context_servers() -> HashMap<String, ContextServerConfig> {
|
||||
DEFAULT_CONFIG.context_servers.clone()
|
||||
}
|
||||
|
||||
/// 获取默认的 Agent Servers 配置
|
||||
///
|
||||
/// 从 `configs/default_agents.json` 加载配置。
|
||||
/// 修改 JSON 文件后重新编译即可生效。
|
||||
pub fn default_agent_servers() -> HashMap<String, AgentConfig> {
|
||||
DEFAULT_CONFIG.agent_servers.clone()
|
||||
}
|
||||
|
||||
/// 获取指定 Agent 的默认配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `agent_id`: Agent 标识符
|
||||
///
|
||||
/// # 返回
|
||||
/// 如果存在返回配置的克隆,否则返回 None
|
||||
pub fn get_default_agent(agent_id: &str) -> Option<AgentConfig> {
|
||||
DEFAULT_CONFIG.agent_servers.get(agent_id).cloned()
|
||||
}
|
||||
|
||||
/// 获取指定 Context Server 的默认配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `name`: Context Server 名称
|
||||
///
|
||||
/// # 返回
|
||||
/// 如果存在返回配置的克隆,否则返回 None
|
||||
pub fn get_default_context_server(name: &str) -> Option<ContextServerConfig> {
|
||||
DEFAULT_CONFIG.context_servers.get(name).cloned()
|
||||
}
|
||||
|
||||
/// 根据服务类型获取默认配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `service_type`: 服务类型(RCoder 或 ComputerAgentRunner)
|
||||
///
|
||||
/// # 返回
|
||||
/// 对应服务类型的默认配置引用
|
||||
pub fn get_default_config_by_service_type(
|
||||
service_type: &shared_types::ServiceType,
|
||||
) -> &'static EmbeddedConfig {
|
||||
match service_type {
|
||||
shared_types::ServiceType::RCoder => &DEFAULT_CONFIG,
|
||||
shared_types::ServiceType::ComputerAgentRunner => &COMPUTER_AGENT_CONFIG,
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据服务类型获取默认 Agent Servers 配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `service_type`: 服务类型
|
||||
///
|
||||
/// # 返回
|
||||
/// Agent Servers 配置的克隆
|
||||
pub fn default_agent_servers_for_service(
|
||||
service_type: &shared_types::ServiceType,
|
||||
) -> HashMap<String, AgentConfig> {
|
||||
get_default_config_by_service_type(service_type)
|
||||
.agent_servers
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// 根据服务类型获取默认 Context Servers 配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `service_type`: 服务类型
|
||||
///
|
||||
/// # 返回
|
||||
/// Context Servers 配置的克隆
|
||||
pub fn default_context_servers_for_service(
|
||||
service_type: &shared_types::ServiceType,
|
||||
) -> HashMap<String, ContextServerConfig> {
|
||||
get_default_config_by_service_type(service_type)
|
||||
.context_servers
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// 根据服务类型获取指定 Agent 的默认配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `agent_id`: Agent 标识符
|
||||
/// - `service_type`: 服务类型
|
||||
///
|
||||
/// # 返回
|
||||
/// 如果存在返回配置的克隆,否则返回 None
|
||||
pub fn get_default_agent_for_service(
|
||||
agent_id: &str,
|
||||
service_type: &shared_types::ServiceType,
|
||||
) -> Option<AgentConfig> {
|
||||
get_default_config_by_service_type(service_type)
|
||||
.agent_servers
|
||||
.get(agent_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// 根据服务类型获取指定 Context Server 的默认配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `name`: Context Server 名称
|
||||
/// - `service_type`: 服务类型
|
||||
///
|
||||
/// # 返回
|
||||
/// 如果存在返回配置的克隆,否则返回 None
|
||||
pub fn get_default_context_server_for_service(
|
||||
name: &str,
|
||||
service_type: &shared_types::ServiceType,
|
||||
) -> Option<ContextServerConfig> {
|
||||
get_default_config_by_service_type(service_type)
|
||||
.context_servers
|
||||
.get(name)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config_loads_successfully() {
|
||||
// 确保 JSON 能成功解析
|
||||
let _ = &*DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_claude_code_agent() {
|
||||
let agent = default_claude_code_agent();
|
||||
assert_eq!(agent.agent_id, CLAUDE_CODE_ACP_AGENT_ID);
|
||||
assert_eq!(agent.command, "claude-code-acp-ts");
|
||||
assert!(agent.enabled);
|
||||
assert!(agent.env.contains_key("ANTHROPIC_API_KEY"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_context_servers() {
|
||||
let servers = default_context_servers();
|
||||
// 验证 JSON 中定义的 context servers
|
||||
assert!(servers.contains_key("fetch") || servers.contains_key("context7"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_agent_servers() {
|
||||
let agents = default_agent_servers();
|
||||
assert!(agents.contains_key(CLAUDE_CODE_ACP_AGENT_ID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_default_agent() {
|
||||
let agent = get_default_agent(CLAUDE_CODE_ACP_AGENT_ID);
|
||||
assert!(agent.is_some());
|
||||
assert_eq!(agent.unwrap().agent_id, CLAUDE_CODE_ACP_AGENT_ID);
|
||||
|
||||
let non_existent = get_default_agent("non-existent-agent");
|
||||
assert!(non_existent.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_default_context_server() {
|
||||
// 测试存在的 server
|
||||
let servers = default_context_servers();
|
||||
if let Some(first_server_name) = servers.keys().next() {
|
||||
let server = get_default_context_server(first_server_name);
|
||||
assert!(server.is_some());
|
||||
}
|
||||
|
||||
// 测试不存在的 server
|
||||
let non_existent = get_default_context_server("non-existent-server");
|
||||
assert!(non_existent.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_computer_agent_config_loads_successfully() {
|
||||
// 确保 ComputerAgentRunner 配置能成功解析
|
||||
let _ = &*COMPUTER_AGENT_CONFIG;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_default_config_by_service_type() {
|
||||
// 测试 RCoder 配置
|
||||
let rcoder_config = get_default_config_by_service_type(&shared_types::ServiceType::RCoder);
|
||||
assert!(
|
||||
rcoder_config
|
||||
.agent_servers
|
||||
.contains_key(CLAUDE_CODE_ACP_AGENT_ID)
|
||||
);
|
||||
|
||||
// 测试 ComputerAgentRunner 配置
|
||||
let car_config =
|
||||
get_default_config_by_service_type(&shared_types::ServiceType::ComputerAgentRunner);
|
||||
assert!(
|
||||
car_config
|
||||
.agent_servers
|
||||
.contains_key(CLAUDE_CODE_ACP_AGENT_ID)
|
||||
);
|
||||
|
||||
// ComputerAgentRunner 应该有更多 context servers
|
||||
assert!(
|
||||
car_config.context_servers.contains_key("chrome-devtools")
|
||||
|| car_config.context_servers.contains_key("puppeteer")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_agent_servers_for_service() {
|
||||
// RCoder
|
||||
let rcoder_servers = default_agent_servers_for_service(&shared_types::ServiceType::RCoder);
|
||||
assert!(!rcoder_servers.is_empty());
|
||||
|
||||
// ComputerAgentRunner
|
||||
let car_servers =
|
||||
default_agent_servers_for_service(&shared_types::ServiceType::ComputerAgentRunner);
|
||||
assert!(!car_servers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_context_servers_for_service() {
|
||||
// RCoder - 基础 MCP servers
|
||||
let rcoder_contexts =
|
||||
default_context_servers_for_service(&shared_types::ServiceType::RCoder);
|
||||
assert!(rcoder_contexts.contains_key("fetch") || rcoder_contexts.contains_key("context7"));
|
||||
|
||||
// ComputerAgentRunner - 增强 MCP servers
|
||||
let car_contexts =
|
||||
default_context_servers_for_service(&shared_types::ServiceType::ComputerAgentRunner);
|
||||
// 至少应该包含浏览器相关的 server
|
||||
assert!(car_contexts.len() >= rcoder_contexts.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_default_agent_for_service() {
|
||||
// 测试 RCoder
|
||||
let agent = get_default_agent_for_service(
|
||||
CLAUDE_CODE_ACP_AGENT_ID,
|
||||
&shared_types::ServiceType::RCoder,
|
||||
);
|
||||
assert!(agent.is_some());
|
||||
|
||||
// 测试 ComputerAgentRunner
|
||||
let agent = get_default_agent_for_service(
|
||||
CLAUDE_CODE_ACP_AGENT_ID,
|
||||
&shared_types::ServiceType::ComputerAgentRunner,
|
||||
);
|
||||
assert!(agent.is_some());
|
||||
|
||||
// 测试不存在的 agent
|
||||
let agent =
|
||||
get_default_agent_for_service("non-existent-agent", &shared_types::ServiceType::RCoder);
|
||||
assert!(agent.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_default_context_server_for_service() {
|
||||
// RCoder context server
|
||||
let rcoder_contexts =
|
||||
default_context_servers_for_service(&shared_types::ServiceType::RCoder);
|
||||
if let Some(first_server_name) = rcoder_contexts.keys().next() {
|
||||
let server = get_default_context_server_for_service(
|
||||
first_server_name,
|
||||
&shared_types::ServiceType::RCoder,
|
||||
);
|
||||
assert!(server.is_some());
|
||||
}
|
||||
|
||||
// ComputerAgentRunner context server
|
||||
let _server = get_default_context_server_for_service(
|
||||
"chrome-devtools",
|
||||
&shared_types::ServiceType::ComputerAgentRunner,
|
||||
);
|
||||
// 可能存在或不存在,取决于配置
|
||||
|
||||
// 测试不存在的 server
|
||||
let server = get_default_context_server_for_service(
|
||||
"non-existent-server",
|
||||
&shared_types::ServiceType::RCoder,
|
||||
);
|
||||
assert!(server.is_none());
|
||||
}
|
||||
}
|
||||
13
qiming-rcoder/crates/agent_config/src/config/mod.rs
Normal file
13
qiming-rcoder/crates/agent_config/src/config/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
//! Configuration management module.
|
||||
//!
|
||||
//! - `default_agent_config`: 默认 Agent 配置定义
|
||||
//! - `servers_config`: 配置文件加载和管理
|
||||
//! - `prompt_assembler`: 提示词配置组装工具
|
||||
|
||||
pub mod default_agent_config;
|
||||
pub mod prompt_assembler;
|
||||
pub mod servers_config;
|
||||
|
||||
pub use default_agent_config::*;
|
||||
pub use prompt_assembler::PromptConfigAssembler;
|
||||
pub use servers_config::*;
|
||||
384
qiming-rcoder/crates/agent_config/src/config/prompt_assembler.rs
Normal file
384
qiming-rcoder/crates/agent_config/src/config/prompt_assembler.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
//! 提示词配置组装工具
|
||||
//!
|
||||
//! 将用户入参组装为内部使用的配置结构。
|
||||
//! 简化设计:直接使用入参,无优先级冲突。
|
||||
|
||||
use shared_types::{ChatAgentConfig, ChatAgentServerConfig};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::servers_config::AgentServersConfig;
|
||||
use crate::types::agent_config::AgentConfig;
|
||||
use crate::types::mcp_config::ContextServerConfig;
|
||||
|
||||
/// 提示词配置组装器
|
||||
///
|
||||
/// 职责:
|
||||
/// 1. 组装系统提示词(入参 > 默认配置)
|
||||
/// 2. 应用用户提示词模板
|
||||
/// 3. 组装 Agent 服务器配置
|
||||
/// 4. 合并 MCP 服务器配置
|
||||
pub struct PromptConfigAssembler {
|
||||
/// 系统提示词入参
|
||||
system_prompt: Option<String>,
|
||||
/// 用户提示词模板入参
|
||||
user_prompt_template: Option<String>,
|
||||
/// Agent 运行时配置入参
|
||||
agent_config: Option<ChatAgentConfig>,
|
||||
/// 默认配置
|
||||
default_config: AgentServersConfig,
|
||||
}
|
||||
|
||||
impl PromptConfigAssembler {
|
||||
/// 创建新的配置组装器
|
||||
///
|
||||
/// # 参数
|
||||
/// - `default_config`: 默认配置(从文件或内置配置加载)
|
||||
pub fn new(default_config: AgentServersConfig) -> Self {
|
||||
Self {
|
||||
system_prompt: None,
|
||||
user_prompt_template: None,
|
||||
agent_config: None,
|
||||
default_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置系统提示词覆盖
|
||||
pub fn with_system_prompt(mut self, system_prompt: Option<String>) -> Self {
|
||||
self.system_prompt = system_prompt;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置用户提示词模板覆盖
|
||||
pub fn with_user_prompt_template(mut self, template: Option<String>) -> Self {
|
||||
self.user_prompt_template = template;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置 Agent 运行时配置覆盖
|
||||
pub fn with_agent_config(mut self, config: Option<ChatAgentConfig>) -> Self {
|
||||
self.agent_config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// 获取最终的系统提示词
|
||||
///
|
||||
/// 逻辑:入参有值则使用入参,否则使用默认配置
|
||||
pub fn get_system_prompt(&self, agent_id: &str) -> String {
|
||||
// 入参有值且非空,直接使用
|
||||
if let Some(ref sp) = self.system_prompt {
|
||||
if !sp.is_empty() {
|
||||
return sp.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用默认配置
|
||||
self.default_config.get_system_prompt(agent_id)
|
||||
}
|
||||
|
||||
/// 应用用户提示词模板
|
||||
///
|
||||
/// 逻辑:
|
||||
/// 1. 如果有模板入参,使用模板替换 `{user_prompt}`
|
||||
/// 2. 如果没有模板入参,检查默认配置
|
||||
/// 3. 都没有,直接返回原始输入
|
||||
pub fn apply_user_prompt(&self, agent_id: &str, user_input: &str) -> String {
|
||||
// 入参有模板且非空,使用入参模板
|
||||
if let Some(ref template) = self.user_prompt_template {
|
||||
if !template.is_empty() {
|
||||
return template.replace("{user_prompt}", user_input);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查默认配置中的 user_prompt 模板
|
||||
if let Some(agent) = self.default_config.get_agent(agent_id) {
|
||||
if let Some(ref prompt_config) = agent.user_prompt {
|
||||
if prompt_config.enabled {
|
||||
return prompt_config.apply(user_input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 无模板,直接返回原始输入
|
||||
user_input.to_string()
|
||||
}
|
||||
|
||||
/// 获取最终的 Agent 服务器配置
|
||||
///
|
||||
/// 逻辑:
|
||||
/// 1. 如果入参有 agent_server,与默认配置合并(入参字段覆盖默认值)
|
||||
/// 2. 如果入参没有 agent_server,使用默认配置
|
||||
pub fn get_agent_server_config(&self, default_agent_id: &str) -> AgentConfig {
|
||||
// 获取默认的 Agent 配置
|
||||
let default_agent = self
|
||||
.default_config
|
||||
.get_agent(default_agent_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// 如果入参有 agent_server 配置,合并覆盖
|
||||
if let Some(ref config) = self.agent_config {
|
||||
if let Some(ref agent_server) = config.agent_server {
|
||||
return self.merge_agent_config(&default_agent, agent_server);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用默认配置
|
||||
default_agent
|
||||
}
|
||||
|
||||
/// 合并 Agent 配置(入参覆盖默认值)
|
||||
fn merge_agent_config(
|
||||
&self,
|
||||
default: &AgentConfig,
|
||||
override_config: &ChatAgentServerConfig,
|
||||
) -> AgentConfig {
|
||||
let mut merged = default.clone();
|
||||
|
||||
// agent_id: 入参有值则覆盖
|
||||
if let Some(ref agent_id) = override_config.agent_id {
|
||||
merged.agent_id = agent_id.clone();
|
||||
}
|
||||
|
||||
// command: 入参有值则覆盖
|
||||
if let Some(ref command) = override_config.command {
|
||||
merged.command = command.clone();
|
||||
}
|
||||
|
||||
// args: 入参有值则覆盖(替换而非追加)
|
||||
if let Some(ref args) = override_config.args {
|
||||
merged.args = args.clone();
|
||||
}
|
||||
|
||||
// env: 入参有值则合并(入参优先)
|
||||
if let Some(ref env) = override_config.env {
|
||||
for (key, value) in env {
|
||||
merged.env.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// metadata: 入参有值则合并
|
||||
if let Some(ref metadata) = override_config.metadata {
|
||||
for (key, value) in metadata {
|
||||
merged.metadata.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
merged
|
||||
}
|
||||
|
||||
/// 获取最终的 MCP 服务器配置
|
||||
///
|
||||
/// 逻辑:
|
||||
/// 1. 如果入参明确提供了非空的 context_servers,使用入参
|
||||
/// 2. 否则使用默认配置
|
||||
///
|
||||
/// 注意:即使提供了 agent_config,但 context_servers 为空时,仍使用默认配置
|
||||
pub fn get_context_servers(&self) -> HashMap<String, ContextServerConfig> {
|
||||
// 入参有非空的 MCP 配置,使用入参
|
||||
if let Some(ref config) = self.agent_config {
|
||||
if config.has_context_servers() {
|
||||
return config
|
||||
.context_servers
|
||||
.iter()
|
||||
.map(|(name, chat_config)| {
|
||||
let ctx_config = ContextServerConfig {
|
||||
source: chat_config.source.clone(),
|
||||
enabled: chat_config.enabled,
|
||||
command: chat_config.command.clone(),
|
||||
args: chat_config.args.clone(),
|
||||
env: chat_config.env.clone(),
|
||||
};
|
||||
(name.clone(), ctx_config)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
// context_servers 为空或未提供,使用默认配置
|
||||
self.default_config.context_servers.clone()
|
||||
}
|
||||
|
||||
/// 获取使用的 Agent ID
|
||||
///
|
||||
/// 逻辑:入参有指定则使用入参,否则使用默认
|
||||
pub fn get_agent_id(&self, default_agent_id: &str) -> String {
|
||||
if let Some(ref config) = self.agent_config {
|
||||
if let Some(ref agent_server) = config.agent_server {
|
||||
return agent_server.get_agent_id().to_string();
|
||||
}
|
||||
}
|
||||
default_agent_id.to_string()
|
||||
}
|
||||
|
||||
/// 检查是否有系统提示词覆盖
|
||||
pub fn has_system_prompt_override(&self) -> bool {
|
||||
self.system_prompt.as_ref().is_some_and(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// 检查是否有用户提示词模板覆盖
|
||||
pub fn has_user_prompt_template_override(&self) -> bool {
|
||||
self.user_prompt_template
|
||||
.as_ref()
|
||||
.is_some_and(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// 检查是否有 Agent 配置覆盖
|
||||
pub fn has_agent_config_override(&self) -> bool {
|
||||
self.agent_config.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_default_config() -> AgentServersConfig {
|
||||
AgentServersConfig::default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_prompt_with_override() {
|
||||
let config = create_test_default_config();
|
||||
let assembler = PromptConfigAssembler::new(config)
|
||||
.with_system_prompt(Some("自定义系统提示词".to_string()));
|
||||
|
||||
let result = assembler.get_system_prompt("claude-code-acp-ts");
|
||||
assert_eq!(result, "自定义系统提示词");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_prompt_without_override() {
|
||||
let config = create_test_default_config();
|
||||
let assembler = PromptConfigAssembler::new(config);
|
||||
|
||||
let result = assembler.get_system_prompt("claude-code-acp-ts");
|
||||
// 应该返回默认系统提示词
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_prompt_empty_override() {
|
||||
let config = create_test_default_config();
|
||||
let assembler = PromptConfigAssembler::new(config).with_system_prompt(Some("".to_string()));
|
||||
|
||||
let result = assembler.get_system_prompt("claude-code-acp-ts");
|
||||
// 空字符串应该回退到默认值
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_prompt_with_template() {
|
||||
let config = create_test_default_config();
|
||||
let assembler = PromptConfigAssembler::new(config)
|
||||
.with_user_prompt_template(Some("请用 Rust 完成:{user_prompt}".to_string()));
|
||||
|
||||
let result = assembler.apply_user_prompt("claude-code-acp-ts", "Hello World");
|
||||
assert_eq!(result, "请用 Rust 完成:Hello World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_prompt_without_template() {
|
||||
let config = create_test_default_config();
|
||||
let assembler = PromptConfigAssembler::new(config);
|
||||
|
||||
let result = assembler.apply_user_prompt("claude-code-acp-ts", "Hello World");
|
||||
// 没有模板时应该返回原始输入
|
||||
assert_eq!(result, "Hello World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_servers_with_override() {
|
||||
let config = create_test_default_config();
|
||||
let mut context_servers = HashMap::new();
|
||||
context_servers.insert(
|
||||
"my-mcp".to_string(),
|
||||
shared_types::ChatContextServerConfig {
|
||||
source: "custom".to_string(),
|
||||
enabled: true,
|
||||
command: Some("bunx".to_string()),
|
||||
args: Some(vec!["-y".to_string(), "my-mcp-server".to_string()]),
|
||||
env: None,
|
||||
},
|
||||
);
|
||||
|
||||
let agent_config = ChatAgentConfig {
|
||||
agent_server: None,
|
||||
context_servers,
|
||||
resource_limits: None,
|
||||
};
|
||||
|
||||
let assembler = PromptConfigAssembler::new(config).with_agent_config(Some(agent_config));
|
||||
|
||||
let result = assembler.get_context_servers();
|
||||
assert!(result.contains_key("my-mcp"));
|
||||
assert_eq!(
|
||||
result.get("my-mcp").unwrap().command,
|
||||
Some("bunx".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_servers_without_override() {
|
||||
let config = create_test_default_config();
|
||||
let assembler = PromptConfigAssembler::new(config);
|
||||
|
||||
let result = assembler.get_context_servers();
|
||||
// 应该返回默认的 context servers
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_id_with_override() {
|
||||
let config = create_test_default_config();
|
||||
let agent_config = ChatAgentConfig {
|
||||
agent_server: Some(shared_types::ChatAgentServerConfig {
|
||||
agent_id: Some("custom-agent".to_string()),
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
metadata: None,
|
||||
}),
|
||||
context_servers: HashMap::new(),
|
||||
resource_limits: None,
|
||||
};
|
||||
|
||||
let assembler = PromptConfigAssembler::new(config).with_agent_config(Some(agent_config));
|
||||
|
||||
let result = assembler.get_agent_id("claude-code-acp-ts");
|
||||
assert_eq!(result, "custom-agent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_id_without_override() {
|
||||
let config = create_test_default_config();
|
||||
let assembler = PromptConfigAssembler::new(config);
|
||||
|
||||
let result = assembler.get_agent_id("claude-code-acp-ts");
|
||||
assert_eq!(result, "claude-code-acp-ts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_agent_config_env() {
|
||||
let config = create_test_default_config();
|
||||
let mut override_env = HashMap::new();
|
||||
override_env.insert("NEW_VAR".to_string(), "new_value".to_string());
|
||||
|
||||
let agent_config = ChatAgentConfig {
|
||||
agent_server: Some(shared_types::ChatAgentServerConfig {
|
||||
agent_id: None,
|
||||
command: None,
|
||||
args: None,
|
||||
env: Some(override_env),
|
||||
metadata: None,
|
||||
}),
|
||||
context_servers: HashMap::new(),
|
||||
resource_limits: None,
|
||||
};
|
||||
|
||||
let assembler = PromptConfigAssembler::new(config).with_agent_config(Some(agent_config));
|
||||
|
||||
let result = assembler.get_agent_server_config("claude-code-acp-ts");
|
||||
assert!(result.env.contains_key("NEW_VAR"));
|
||||
assert_eq!(result.env.get("NEW_VAR"), Some(&"new_value".to_string()));
|
||||
}
|
||||
}
|
||||
226
qiming-rcoder/crates/agent_config/src/config/servers_config.rs
Normal file
226
qiming-rcoder/crates/agent_config/src/config/servers_config.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
//! Agent servers configuration structure.
|
||||
//!
|
||||
//! 此模块负责配置的查询和管理。
|
||||
//! 默认配置从 `default_agent_config` 模块加载(来源于 `configs/default_agents.json`)。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use super::default_agent_config::{
|
||||
default_agent_servers, default_agent_servers_for_service, default_context_servers,
|
||||
default_context_servers_for_service,
|
||||
};
|
||||
use crate::types::agent_config::AgentConfig;
|
||||
use crate::types::error::{ConfigError, Result};
|
||||
use crate::types::mcp_config::ContextServerConfig;
|
||||
use crate::types::system_prompt::DEFAULT_SYSTEM_PROMPT;
|
||||
|
||||
/// Agent servers configuration
|
||||
///
|
||||
/// 包含所有 Agent 和 Context Server 的配置集合。
|
||||
/// 默认配置来自 `configs/default_agents.json`(编译时嵌入)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentServersConfig {
|
||||
/// Agent servers configuration
|
||||
pub agent_servers: HashMap<String, AgentConfig>,
|
||||
|
||||
/// Context servers configuration (MCP servers)
|
||||
#[serde(default)]
|
||||
pub context_servers: HashMap<String, ContextServerConfig>,
|
||||
}
|
||||
|
||||
impl AgentServersConfig {
|
||||
/// 获取默认配置
|
||||
///
|
||||
/// 返回从 `configs/default_agents.json` 加载的配置。
|
||||
///
|
||||
/// # 已弃用
|
||||
///
|
||||
/// 此方法已弃用,请使用 `load_or_default_for_service` 以支持多服务类型。
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "请使用 load_or_default_for_service 以支持多服务类型"
|
||||
)]
|
||||
pub async fn load_or_default() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// 根据服务类型加载或使用默认配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `service_type`: 服务类型(RCoder 或 ComputerAgentRunner)
|
||||
///
|
||||
/// # 返回
|
||||
/// 对应服务类型的默认配置
|
||||
pub async fn load_or_default_for_service(service_type: &shared_types::ServiceType) -> Self {
|
||||
Self::default_for_service(service_type)
|
||||
}
|
||||
|
||||
/// 根据服务类型创建默认配置
|
||||
///
|
||||
/// # 参数
|
||||
/// - `service_type`: 服务类型
|
||||
///
|
||||
/// # 返回
|
||||
/// 对应服务类型的默认配置实例
|
||||
pub fn default_for_service(service_type: &shared_types::ServiceType) -> Self {
|
||||
Self {
|
||||
agent_servers: default_agent_servers_for_service(service_type),
|
||||
context_servers: default_context_servers_for_service(service_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// 从外部文件加载配置
|
||||
///
|
||||
/// 用于加载用户自定义的配置文件,覆盖默认配置。
|
||||
pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
return Err(ConfigError::file_not_found(path.display()).into());
|
||||
}
|
||||
|
||||
let content = tokio::fs::read_to_string(path)
|
||||
.await
|
||||
.map_err(|e| ConfigError::Read(format!("{}: {}", path.display(), e)))?;
|
||||
|
||||
Self::from_json(&content)
|
||||
}
|
||||
|
||||
/// 从 JSON 字符串加载配置
|
||||
///
|
||||
/// 用于解析用户传入的 JSON 配置。
|
||||
pub fn from_json(json: &str) -> Result<Self> {
|
||||
serde_json::from_str(json).map_err(|e| ConfigError::Deserialization(e.to_string()).into())
|
||||
}
|
||||
|
||||
/// Validate configuration
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
// Validate agent servers
|
||||
for (name, config) in &self.agent_servers {
|
||||
if config.agent_id.is_empty() {
|
||||
return Err(
|
||||
ConfigError::missing_field(format!("agent_servers.{}.agent_id", name)).into(),
|
||||
);
|
||||
}
|
||||
if config.command.is_empty() {
|
||||
return Err(
|
||||
ConfigError::missing_field(format!("agent_servers.{}.command", name)).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate context servers
|
||||
for (name, config) in &self.context_servers {
|
||||
if config.command.is_none() {
|
||||
return Err(ConfigError::missing_field(format!(
|
||||
"context_servers.{}.command",
|
||||
name
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all enabled agents
|
||||
pub fn get_enabled_agents(&self) -> Vec<&AgentConfig> {
|
||||
self.agent_servers
|
||||
.values()
|
||||
.filter(|config| config.enabled)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get an agent configuration by ID
|
||||
pub fn get_agent(&self, agent_id: &str) -> Option<&AgentConfig> {
|
||||
self.agent_servers.get(agent_id)
|
||||
}
|
||||
|
||||
/// 获取系统提示词(优先使用配置,否则使用默认值)
|
||||
///
|
||||
/// # 参数
|
||||
/// - `agent_id`: Agent 标识符
|
||||
///
|
||||
/// # 返回
|
||||
/// 根据配置的 source 字段决定返回内容:
|
||||
/// - `source: "embedded"` -> 返回编译时嵌入的默认提示词
|
||||
/// - `source: "custom"` 且 template 非空 -> 返回自定义模板内容
|
||||
/// - 其他情况 -> 回退到编译时嵌入的默认提示词
|
||||
///
|
||||
/// 注意:此方法始终返回有效的系统提示词,不受 enabled 字段影响。
|
||||
/// 如需检查是否启用,请直接调用 `SystemPromptConfig::get_prompt()`。
|
||||
pub fn get_system_prompt(&self, agent_id: &str) -> String {
|
||||
if let Some(agent) = self.get_agent(agent_id) {
|
||||
// 如果有系统提示词配置,使用 get_prompt_or_default() 确保始终返回有效提示词
|
||||
if let Some(ref prompt_config) = agent.system_prompt {
|
||||
return prompt_config.get_prompt_or_default().to_string();
|
||||
}
|
||||
}
|
||||
// 回退到编译时嵌入的默认值
|
||||
DEFAULT_SYSTEM_PROMPT.to_string()
|
||||
}
|
||||
|
||||
/// Get all enabled context servers
|
||||
pub fn get_enabled_context_servers(&self) -> Vec<&ContextServerConfig> {
|
||||
self.context_servers
|
||||
.values()
|
||||
.filter(|config| config.enabled)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a context server configuration by name
|
||||
pub fn get_context_server(&self, name: &str) -> Option<&ContextServerConfig> {
|
||||
self.context_servers.get(name)
|
||||
}
|
||||
|
||||
/// 创建默认配置
|
||||
///
|
||||
/// 从 `configs/default_agents.json` 加载的配置(编译时嵌入)。
|
||||
/// 修改 JSON 文件后重新编译即可生效。
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
agent_servers: default_agent_servers(),
|
||||
context_servers: default_context_servers(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_or_default_for_service() {
|
||||
// 测试 RCoder
|
||||
let rcoder_config =
|
||||
AgentServersConfig::load_or_default_for_service(&shared_types::ServiceType::RCoder)
|
||||
.await;
|
||||
assert!(!rcoder_config.agent_servers.is_empty());
|
||||
|
||||
// 测试 ComputerAgentRunner
|
||||
let car_config = AgentServersConfig::load_or_default_for_service(
|
||||
&shared_types::ServiceType::ComputerAgentRunner,
|
||||
)
|
||||
.await;
|
||||
assert!(!car_config.agent_servers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_for_service() {
|
||||
// 测试 RCoder
|
||||
let rcoder_config =
|
||||
AgentServersConfig::default_for_service(&shared_types::ServiceType::RCoder);
|
||||
assert!(!rcoder_config.agent_servers.is_empty());
|
||||
|
||||
// 测试 ComputerAgentRunner
|
||||
let car_config = AgentServersConfig::default_for_service(
|
||||
&shared_types::ServiceType::ComputerAgentRunner,
|
||||
);
|
||||
assert!(!car_config.agent_servers.is_empty());
|
||||
|
||||
// 验证配置不同
|
||||
// ComputerAgentRunner 应该有更多或不同的 context servers
|
||||
assert!(car_config.context_servers.len() >= rcoder_config.context_servers.len());
|
||||
}
|
||||
}
|
||||
305
qiming-rcoder/crates/agent_config/src/installer/manager.rs
Normal file
305
qiming-rcoder/crates/agent_config/src/installer/manager.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
//! Agent Installation Manager
|
||||
//!
|
||||
//! Manages agent installation, validation, and updates.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::npm_installer::NpmInstaller;
|
||||
use super::traits::AgentInstaller;
|
||||
use crate::types::installation::{InstallationConfig, PackageManager};
|
||||
|
||||
/// Installation error types
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InstallationError {
|
||||
#[error("Package manager '{0}' is not available")]
|
||||
PackageManagerNotAvailable(String),
|
||||
|
||||
#[error("Package '{package}' not found in {manager}")]
|
||||
PackageNotFound { package: String, manager: String },
|
||||
|
||||
#[error("Installation failed for '{package}': {reason}")]
|
||||
InstallFailed { package: String, reason: String },
|
||||
|
||||
#[error("Validation failed: {0}")]
|
||||
ValidationFailed(String),
|
||||
|
||||
#[error("Command failed: {command} - {reason}")]
|
||||
CommandFailed { command: String, reason: String },
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
ConfigError(String),
|
||||
|
||||
#[error("Unsupported package manager: {0}")]
|
||||
UnsupportedPackageManager(String),
|
||||
}
|
||||
|
||||
/// Installation result
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InstallResult {
|
||||
/// Whether installation was successful
|
||||
pub success: bool,
|
||||
/// Installed version
|
||||
pub version: Option<String>,
|
||||
/// Installation path
|
||||
pub install_path: Option<String>,
|
||||
/// Additional message
|
||||
pub message: String,
|
||||
/// Whether the agent was already installed
|
||||
pub already_installed: bool,
|
||||
}
|
||||
|
||||
impl InstallResult {
|
||||
/// Create a successful installation result
|
||||
pub fn success(version: Option<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
version,
|
||||
install_path: None,
|
||||
message: message.into(),
|
||||
already_installed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a result indicating already installed
|
||||
pub fn already_installed(version: Option<String>) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
version,
|
||||
install_path: None,
|
||||
message: "Already installed".to_string(),
|
||||
already_installed: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a failed installation result
|
||||
pub fn failed(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
version: None,
|
||||
install_path: None,
|
||||
message: message.into(),
|
||||
already_installed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set installation path
|
||||
pub fn with_path(mut self, path: impl Into<String>) -> Self {
|
||||
self.install_path = Some(path.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent Installation Manager
|
||||
///
|
||||
/// Manages multiple package manager installers and handles
|
||||
/// agent installation lifecycle.
|
||||
pub struct AgentInstallationManager {
|
||||
installers: HashMap<String, Arc<dyn AgentInstaller>>,
|
||||
}
|
||||
|
||||
impl AgentInstallationManager {
|
||||
/// Create a new installation manager with default installers
|
||||
pub fn new() -> Self {
|
||||
let mut installers: HashMap<String, Arc<dyn AgentInstaller>> = HashMap::new();
|
||||
|
||||
// Register npm installer
|
||||
installers.insert("npm".to_string(), Arc::new(NpmInstaller::new()));
|
||||
|
||||
Self { installers }
|
||||
}
|
||||
|
||||
/// Register a custom installer
|
||||
pub fn register_installer(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
installer: Arc<dyn AgentInstaller>,
|
||||
) {
|
||||
self.installers.insert(name.into(), installer);
|
||||
}
|
||||
|
||||
/// Get installer for package manager
|
||||
fn get_installer(&self, pm: &PackageManager) -> Option<Arc<dyn AgentInstaller>> {
|
||||
let name = pm.as_str();
|
||||
self.installers.get(name).cloned()
|
||||
}
|
||||
|
||||
/// Ensure agent is installed
|
||||
///
|
||||
/// This method checks if the agent is already installed and working.
|
||||
/// If not, it attempts to install it.
|
||||
pub async fn ensure_installed(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
command: &str,
|
||||
) -> Result<InstallResult, InstallationError> {
|
||||
info!("Checking Agent installation status: {}", command);
|
||||
|
||||
// First, check if the command already exists
|
||||
if self.is_command_available(command).await {
|
||||
// Validate if it's working correctly
|
||||
if self.validate_installation(config, command).await? {
|
||||
info!("Agent is installed and verified: {}", command);
|
||||
return Ok(InstallResult::already_installed(None));
|
||||
}
|
||||
warn!(
|
||||
"Agent command exists but verification failed, trying reinstall: {}",
|
||||
command
|
||||
);
|
||||
}
|
||||
|
||||
// Not installed or validation failed, try to install
|
||||
info!("Trying to install Agent: {}", command);
|
||||
let result = self.install(config, None).await;
|
||||
|
||||
match &result {
|
||||
Ok(install_result) => {
|
||||
if install_result.success {
|
||||
info!(
|
||||
"Agent installed successfully: {} - {}",
|
||||
command, install_result.message
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
"⚠️ Agent Installation failed: {} - {}",
|
||||
command, install_result.message
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Agent installation error: {} - {}", command, e);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Install agent using appropriate package manager
|
||||
pub async fn install(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
install_dir: Option<&Path>,
|
||||
) -> Result<InstallResult, InstallationError> {
|
||||
let installer = self.get_installer(&config.package_manager).ok_or_else(|| {
|
||||
InstallationError::UnsupportedPackageManager(
|
||||
config.package_manager.as_str().to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let package_name = config.package_name.as_ref().ok_or_else(|| {
|
||||
InstallationError::ConfigError("Package name is required".to_string())
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"Starting Agent installation: {} via {}",
|
||||
package_name,
|
||||
installer.package_manager_name()
|
||||
);
|
||||
|
||||
installer.install(config, install_dir).await
|
||||
}
|
||||
|
||||
/// Validate agent installation
|
||||
pub async fn validate_installation(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
command: &str,
|
||||
) -> Result<bool, InstallationError> {
|
||||
debug!("Verifying Agent installation: {}", command);
|
||||
|
||||
// If validate_command is specified, use it
|
||||
if let Some(validate_cmd) = &config.validate_command {
|
||||
if !validate_cmd.is_empty() {
|
||||
return self.run_validation_command(validate_cmd).await;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 which 检查命令是否可用(适用于 Debian/Docker 环境)
|
||||
// which::which 是 Rust crate,不依赖系统 which 命令
|
||||
// 注意:只检查命令是否在 PATH 中,不运行命令避免副作用(如 claude-code-acp-ts --version 会阻塞)
|
||||
if self.is_command_available(command).await {
|
||||
debug!("Command exists in PATH: {}", command);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
debug!("Command not in PATH: {}", command);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Check if a command is available in PATH
|
||||
async fn is_command_available(&self, command: &str) -> bool {
|
||||
which::which(command).is_ok()
|
||||
}
|
||||
|
||||
/// Run custom validation command
|
||||
async fn run_validation_command(&self, cmd: &[String]) -> Result<bool, InstallationError> {
|
||||
if cmd.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let program = &cmd[0];
|
||||
let args: Vec<&str> = cmd[1..].iter().map(|s| s.as_str()).collect();
|
||||
|
||||
debug!("Running verification command: {} {:?}", program, args);
|
||||
|
||||
let output = tokio::process::Command::new(program)
|
||||
.args(&args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| InstallationError::CommandFailed {
|
||||
command: format!("{} {}", program, args.join(" ")),
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(output.status.success())
|
||||
}
|
||||
|
||||
/// Update agent to latest version
|
||||
pub async fn update(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
install_dir: Option<&Path>,
|
||||
) -> Result<InstallResult, InstallationError> {
|
||||
let installer = self.get_installer(&config.package_manager).ok_or_else(|| {
|
||||
InstallationError::UnsupportedPackageManager(
|
||||
config.package_manager.as_str().to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("Updating Agent via {}", installer.package_manager_name());
|
||||
|
||||
installer.update(config, install_dir).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AgentInstallationManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_command_availability() {
|
||||
let manager = AgentInstallationManager::new();
|
||||
// Test with a command that should exist on most systems
|
||||
let ls_available = manager.is_command_available("ls").await;
|
||||
assert!(ls_available);
|
||||
|
||||
// Test with a command that shouldn't exist
|
||||
let fake_available = manager
|
||||
.is_command_available("this_command_does_not_exist_12345")
|
||||
.await;
|
||||
assert!(!fake_available);
|
||||
}
|
||||
}
|
||||
11
qiming-rcoder/crates/agent_config/src/installer/mod.rs
Normal file
11
qiming-rcoder/crates/agent_config/src/installer/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! Agent Installation Management
|
||||
//!
|
||||
//! This module provides automatic installation and validation for agents.
|
||||
|
||||
mod manager;
|
||||
mod npm_installer;
|
||||
mod traits;
|
||||
|
||||
pub use manager::{AgentInstallationManager, InstallResult, InstallationError};
|
||||
pub use npm_installer::NpmInstaller;
|
||||
pub use traits::AgentInstaller;
|
||||
236
qiming-rcoder/crates/agent_config/src/installer/npm_installer.rs
Normal file
236
qiming-rcoder/crates/agent_config/src/installer/npm_installer.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
//! NPM Package Installer
|
||||
//!
|
||||
//! Implements agent installation via npm.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::manager::{InstallResult, InstallationError};
|
||||
use super::traits::AgentInstaller;
|
||||
use crate::types::installation::InstallationConfig;
|
||||
|
||||
/// NPM Installer
|
||||
///
|
||||
/// Installs agents via npm global installation.
|
||||
pub struct NpmInstaller {
|
||||
/// Use pnpm instead of npm if available
|
||||
prefer_pnpm: bool,
|
||||
}
|
||||
|
||||
impl NpmInstaller {
|
||||
/// Create a new NPM installer
|
||||
pub fn new() -> Self {
|
||||
Self { prefer_pnpm: false }
|
||||
}
|
||||
|
||||
/// Create NPM installer that prefers pnpm
|
||||
pub fn with_pnpm_preference() -> Self {
|
||||
Self { prefer_pnpm: true }
|
||||
}
|
||||
|
||||
/// Get the npm command to use (npm or pnpm)
|
||||
async fn get_npm_command(&self) -> &'static str {
|
||||
if self.prefer_pnpm && which::which("pnpm").is_ok() {
|
||||
"pnpm"
|
||||
} else if which::which("npm").is_ok() {
|
||||
"npm"
|
||||
} else {
|
||||
"npm" // Default, will fail if not available
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse version from npm output
|
||||
fn parse_installed_version(&self, output: &str) -> Option<String> {
|
||||
// npm list output format: "package@version"
|
||||
for line in output.lines() {
|
||||
if line.contains('@') {
|
||||
if let Some(version) = line.split('@').last() {
|
||||
return Some(version.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NpmInstaller {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AgentInstaller for NpmInstaller {
|
||||
async fn install(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
_install_dir: Option<&Path>,
|
||||
) -> Result<InstallResult, InstallationError> {
|
||||
let npm_cmd = self.get_npm_command().await;
|
||||
|
||||
// Check if npm is available
|
||||
if !self.command_exists(npm_cmd).await {
|
||||
return Err(InstallationError::PackageManagerNotAvailable(
|
||||
npm_cmd.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let package_name = config.package_name.as_ref().ok_or_else(|| {
|
||||
InstallationError::ConfigError("Package name is required".to_string())
|
||||
})?;
|
||||
|
||||
// Build package spec with version
|
||||
let package_spec = if let Some(version) = &config.version {
|
||||
if version == "latest" {
|
||||
package_name.clone()
|
||||
} else {
|
||||
format!("{}@{}", package_name, version)
|
||||
}
|
||||
} else {
|
||||
package_name.clone()
|
||||
};
|
||||
|
||||
info!(" running {} : {}", npm_cmd, package_spec);
|
||||
|
||||
// Run npm install -g
|
||||
// 仅 nuwaxcode 使用官方源(--registry 参数优先级高于 .npmrc)
|
||||
let output = if package_name == "nuwaxcode" {
|
||||
info!("🌐 nuwaxcode using registry: https://registry.npmjs.org/");
|
||||
self.run_command(
|
||||
npm_cmd,
|
||||
&[
|
||||
"install",
|
||||
"-g",
|
||||
&package_spec,
|
||||
"--registry=https://registry.npmjs.org/",
|
||||
],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
self.run_command(npm_cmd, &["install", "-g", &package_spec])
|
||||
.await?
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
debug!("npm install stdout: {}", stdout);
|
||||
if !stderr.is_empty() {
|
||||
debug!("npm install stderr: {}", stderr);
|
||||
}
|
||||
|
||||
// Try to get installed version
|
||||
let version = self.parse_installed_version(&stdout);
|
||||
|
||||
info!("Package installed: {}", package_spec);
|
||||
Ok(InstallResult::success(
|
||||
version,
|
||||
format!("Successfully installed {}", package_spec),
|
||||
))
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
warn!("Installation failed: {}", stderr);
|
||||
Err(InstallationError::InstallFailed {
|
||||
package: package_spec,
|
||||
reason: stderr.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate(&self, config: &InstallationConfig) -> Result<bool, InstallationError> {
|
||||
// Check if the package is globally installed
|
||||
let npm_cmd = self.get_npm_command().await;
|
||||
|
||||
if !self.command_exists(npm_cmd).await {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let package_name = match &config.package_name {
|
||||
Some(name) => name,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
// Check global list
|
||||
let output = self
|
||||
.run_command(npm_cmd, &["list", "-g", "--depth=0", package_name])
|
||||
.await?;
|
||||
|
||||
Ok(output.status.success())
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
_install_dir: Option<&Path>,
|
||||
) -> Result<InstallResult, InstallationError> {
|
||||
let npm_cmd = self.get_npm_command().await;
|
||||
|
||||
if !self.command_exists(npm_cmd).await {
|
||||
return Err(InstallationError::PackageManagerNotAvailable(
|
||||
npm_cmd.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let package_name = config.package_name.as_ref().ok_or_else(|| {
|
||||
InstallationError::ConfigError("Package name is required".to_string())
|
||||
})?;
|
||||
|
||||
info!("Updating global package: {}", package_name);
|
||||
|
||||
// Run npm update -g
|
||||
// 仅 nuwaxcode 使用官方源(--registry 参数优先级高于 .npmrc)
|
||||
let output = if package_name == "nuwaxcode" {
|
||||
info!("🌐 nuwaxcode using registry: https://registry.npmjs.org/");
|
||||
self.run_command(
|
||||
npm_cmd,
|
||||
&[
|
||||
"update",
|
||||
"-g",
|
||||
package_name,
|
||||
"--registry=https://registry.npmjs.org/",
|
||||
],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
self.run_command(npm_cmd, &["update", "-g", package_name])
|
||||
.await?
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let version = self.parse_installed_version(&stdout);
|
||||
|
||||
info!("Update succeeded: {}", package_name);
|
||||
Ok(InstallResult::success(
|
||||
version,
|
||||
format!("Successfully updated {}", package_name),
|
||||
))
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
warn!("Update failed: {}", stderr);
|
||||
Err(InstallationError::InstallFailed {
|
||||
package: package_name.clone(),
|
||||
reason: stderr.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn package_manager_name(&self) -> &'static str {
|
||||
"npm"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_npm_command_detection() {
|
||||
let installer = NpmInstaller::new();
|
||||
let cmd = installer.get_npm_command().await;
|
||||
// Should return "npm" or "pnpm"
|
||||
assert!(cmd == "npm" || cmd == "pnpm");
|
||||
}
|
||||
}
|
||||
75
qiming-rcoder/crates/agent_config/src/installer/traits.rs
Normal file
75
qiming-rcoder/crates/agent_config/src/installer/traits.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
//! Agent Installer Trait
|
||||
//!
|
||||
//! Defines the interface for agent installers.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use super::manager::{InstallResult, InstallationError};
|
||||
use crate::types::installation::InstallationConfig;
|
||||
|
||||
/// Agent installer trait
|
||||
///
|
||||
/// Defines the interface for installing, validating, and updating agents.
|
||||
#[async_trait::async_trait]
|
||||
pub trait AgentInstaller: Send + Sync {
|
||||
/// Install the agent
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `config` - Installation configuration
|
||||
/// * `install_dir` - Directory to install to (optional, for global installs)
|
||||
///
|
||||
/// # Returns
|
||||
/// Installation result with details
|
||||
async fn install(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
install_dir: Option<&Path>,
|
||||
) -> Result<InstallResult, InstallationError>;
|
||||
|
||||
/// Validate if the agent is installed correctly
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `config` - Installation configuration containing validate_command
|
||||
///
|
||||
/// # Returns
|
||||
/// true if installed and working, false otherwise
|
||||
async fn validate(&self, config: &InstallationConfig) -> Result<bool, InstallationError>;
|
||||
|
||||
/// Update the agent to latest version
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `config` - Installation configuration
|
||||
/// * `install_dir` - Directory where agent is installed
|
||||
///
|
||||
/// # Returns
|
||||
/// Installation result with new version info
|
||||
async fn update(
|
||||
&self,
|
||||
config: &InstallationConfig,
|
||||
install_dir: Option<&Path>,
|
||||
) -> Result<InstallResult, InstallationError>;
|
||||
|
||||
/// Get the package manager name
|
||||
fn package_manager_name(&self) -> &'static str;
|
||||
|
||||
/// Check if a command exists in PATH
|
||||
async fn command_exists(&self, command: &str) -> bool {
|
||||
which::which(command).is_ok()
|
||||
}
|
||||
|
||||
/// Run a command and get output
|
||||
async fn run_command(
|
||||
&self,
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
) -> Result<std::process::Output, InstallationError> {
|
||||
tokio::process::Command::new(command)
|
||||
.args(args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| InstallationError::CommandFailed {
|
||||
command: format!("{} {}", command, args.join(" ")),
|
||||
reason: e.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
33
qiming-rcoder/crates/agent_config/src/lib.rs
Normal file
33
qiming-rcoder/crates/agent_config/src/lib.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Agent Configuration Management
|
||||
//!
|
||||
//! This crate provides configuration management for agents in RCoder.
|
||||
|
||||
pub mod config;
|
||||
pub mod installer;
|
||||
pub mod types;
|
||||
|
||||
// Re-export main types
|
||||
pub use config::prompt_assembler::PromptConfigAssembler;
|
||||
pub use config::servers_config::AgentServersConfig;
|
||||
pub use types::agent_config::AgentConfig;
|
||||
pub use types::agent_spec::AgentSpec;
|
||||
pub use types::error::{AgentConfigError, ConfigError, Result};
|
||||
pub use types::installation::{InstallationConfig, PackageManager};
|
||||
pub use types::mcp_config::{ContextServerConfig, McpServerConfig, McpServerSource};
|
||||
pub use types::prompt_config::{SystemPromptConfig, UserPromptConfig};
|
||||
pub use types::system_prompt::DEFAULT_SYSTEM_PROMPT;
|
||||
#[allow(deprecated)]
|
||||
pub use types::system_prompt::PromptBuilder;
|
||||
|
||||
// Installer exports
|
||||
pub use installer::{
|
||||
AgentInstallationManager, AgentInstaller, InstallResult, InstallationError, NpmInstaller,
|
||||
};
|
||||
|
||||
// Default configuration exports
|
||||
pub use config::default_agent_config::{
|
||||
CLAUDE_CODE_ACP_AGENT_ID, default_agent_servers, default_agent_servers_for_service,
|
||||
default_context_servers, default_context_servers_for_service, get_default_agent,
|
||||
get_default_agent_for_service, get_default_config_by_service_type, get_default_context_server,
|
||||
get_default_context_server_for_service,
|
||||
};
|
||||
115
qiming-rcoder/crates/agent_config/src/types/agent_config.rs
Normal file
115
qiming-rcoder/crates/agent_config/src/types/agent_config.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
//! Agent configuration structure.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_types::ModelProviderConfig;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Agent configuration structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentConfig {
|
||||
/// Agent identifier
|
||||
pub agent_id: String,
|
||||
|
||||
/// Command to execute
|
||||
pub command: String,
|
||||
|
||||
/// Command arguments
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
|
||||
/// Environment variables
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
|
||||
/// Installation configuration
|
||||
pub installation: super::installation::InstallationConfig,
|
||||
|
||||
/// System prompt configuration
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<super::prompt_config::SystemPromptConfig>,
|
||||
|
||||
/// User prompt configuration
|
||||
#[serde(default)]
|
||||
pub user_prompt: Option<super::prompt_config::UserPromptConfig>,
|
||||
|
||||
/// Model provider configuration
|
||||
#[serde(default)]
|
||||
pub model_provider: Option<ModelProviderConfig>,
|
||||
|
||||
/// Whether the agent is enabled
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Metadata
|
||||
#[serde(default)]
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Default enabled value
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for AgentConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
agent_id: String::new(),
|
||||
command: String::new(),
|
||||
args: Vec::new(),
|
||||
env: HashMap::new(),
|
||||
installation: super::installation::InstallationConfig::default(),
|
||||
system_prompt: None,
|
||||
user_prompt: None,
|
||||
model_provider: None,
|
||||
enabled: true,
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConfig {
|
||||
/// Create a new agent config
|
||||
pub fn new(agent_id: String) -> Self {
|
||||
Self {
|
||||
agent_id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get an environment variable
|
||||
pub fn get_env(&self, key: &str) -> Option<&String> {
|
||||
self.env.get(key)
|
||||
}
|
||||
|
||||
/// Set an environment variable
|
||||
pub fn set_env(&mut self, key: String, value: String) {
|
||||
self.env.insert(key, value);
|
||||
}
|
||||
|
||||
/// Check if agent is enabled
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Get model provider configuration
|
||||
pub fn get_model_provider(&self) -> Option<&ModelProviderConfig> {
|
||||
self.model_provider.as_ref()
|
||||
}
|
||||
|
||||
/// Set model provider configuration
|
||||
pub fn set_model_provider(&mut self, provider: ModelProviderConfig) {
|
||||
self.model_provider = Some(provider);
|
||||
}
|
||||
|
||||
/// Get the default model name if configured
|
||||
pub fn get_default_model(&self) -> Option<&str> {
|
||||
self.model_provider
|
||||
.as_ref()
|
||||
.map(|p| p.default_model.as_str())
|
||||
}
|
||||
|
||||
/// Get the model provider name if configured
|
||||
pub fn get_model_provider_name(&self) -> Option<&str> {
|
||||
self.model_provider.as_ref().map(|p| p.name.as_str())
|
||||
}
|
||||
}
|
||||
68
qiming-rcoder/crates/agent_config/src/types/agent_spec.rs
Normal file
68
qiming-rcoder/crates/agent_config/src/types/agent_spec.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Agent specification structure.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Agent specification
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentSpec {
|
||||
/// Agent identifier (matches the key in config file)
|
||||
pub agent_id: String,
|
||||
|
||||
/// Command to execute
|
||||
pub command: String,
|
||||
|
||||
/// Command arguments
|
||||
pub args: Vec<String>,
|
||||
|
||||
/// Environment variables
|
||||
pub env: HashMap<String, String>,
|
||||
|
||||
/// Installation configuration
|
||||
pub installation: super::installation::InstallationConfig,
|
||||
|
||||
/// System prompt configuration
|
||||
pub system_prompt: Option<super::prompt_config::SystemPromptConfig>,
|
||||
|
||||
/// User prompt configuration
|
||||
pub user_prompt: Option<super::prompt_config::UserPromptConfig>,
|
||||
|
||||
/// Whether the agent is enabled
|
||||
pub enabled: bool,
|
||||
|
||||
/// Metadata
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl AgentSpec {
|
||||
/// Create a new agent spec
|
||||
pub fn new(agent_id: String) -> Self {
|
||||
Self {
|
||||
agent_id,
|
||||
command: String::new(),
|
||||
args: Vec::new(),
|
||||
env: HashMap::new(),
|
||||
installation: super::installation::InstallationConfig::default(),
|
||||
system_prompt: None,
|
||||
user_prompt: None,
|
||||
enabled: true,
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to AgentConfig
|
||||
pub fn to_agent_config(&self) -> super::agent_config::AgentConfig {
|
||||
super::agent_config::AgentConfig {
|
||||
agent_id: self.agent_id.clone(),
|
||||
command: self.command.clone(),
|
||||
args: self.args.clone(),
|
||||
env: self.env.clone(),
|
||||
installation: self.installation.clone(),
|
||||
system_prompt: self.system_prompt.clone(),
|
||||
user_prompt: self.user_prompt.clone(),
|
||||
model_provider: None,
|
||||
enabled: self.enabled,
|
||||
metadata: self.metadata.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
168
qiming-rcoder/crates/agent_config/src/types/error.rs
Normal file
168
qiming-rcoder/crates/agent_config/src/types/error.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! Error types for agent configuration.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Agent configuration error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AgentConfigError {
|
||||
/// Configuration error
|
||||
#[error("configuration error: {0}")]
|
||||
Configuration(#[from] ConfigError),
|
||||
|
||||
/// I/O error
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] IoError),
|
||||
|
||||
/// Installation error
|
||||
#[error("installation error: {0}")]
|
||||
Installation(#[from] InstallationError),
|
||||
|
||||
/// Validation error
|
||||
#[error("validation error: {0}")]
|
||||
Validation(#[from] ValidationError),
|
||||
|
||||
/// Other error
|
||||
#[error("other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for AgentConfigError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Configuration(ConfigError::Serialization(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::path::PathBuf> for AgentConfigError {
|
||||
fn from(path: std::path::PathBuf) -> Self {
|
||||
Self::Io(IoError::PathError(path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
/// Serialization error
|
||||
#[error("serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// Deserialization error
|
||||
#[error("deserialization error: {0}")]
|
||||
Deserialization(String),
|
||||
|
||||
/// Missing required field
|
||||
#[error("missing required field: {0}")]
|
||||
MissingField(String),
|
||||
|
||||
/// Invalid value
|
||||
#[error("invalid value: {0}")]
|
||||
InvalidValue(String),
|
||||
|
||||
/// File not found
|
||||
#[error("file not found: {0}")]
|
||||
FileNotFound(String),
|
||||
|
||||
/// Permission denied
|
||||
#[error("permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
|
||||
/// Read error
|
||||
#[error("read error: {0}")]
|
||||
Read(String),
|
||||
}
|
||||
|
||||
impl ConfigError {
|
||||
/// Create a missing field error
|
||||
pub fn missing_field(field: impl Into<String>) -> Self {
|
||||
Self::MissingField(field.into())
|
||||
}
|
||||
|
||||
/// Create an invalid value error
|
||||
pub fn invalid_value(msg: impl Into<String>) -> Self {
|
||||
Self::InvalidValue(msg.into())
|
||||
}
|
||||
|
||||
/// Create a file not found error
|
||||
pub fn file_not_found(path: impl std::fmt::Display) -> Self {
|
||||
Self::FileNotFound(path.to_string())
|
||||
}
|
||||
|
||||
/// Create a read error
|
||||
pub fn read(path: impl std::fmt::Display, source: impl std::fmt::Display) -> Self {
|
||||
Self::Read(format!("{}: {}", path, source))
|
||||
}
|
||||
}
|
||||
|
||||
/// I/O error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum IoError {
|
||||
/// Path error
|
||||
#[error("path error: {0:?}")]
|
||||
PathError(std::path::PathBuf),
|
||||
|
||||
/// Read error
|
||||
#[error("read error: {0}")]
|
||||
Read(String),
|
||||
|
||||
/// Write error
|
||||
#[error("write error: {0}")]
|
||||
Write(String),
|
||||
|
||||
/// Create error
|
||||
#[error("create error: {0}")]
|
||||
Create(String),
|
||||
|
||||
/// Delete error
|
||||
#[error("delete error: {0}")]
|
||||
Delete(String),
|
||||
}
|
||||
|
||||
impl IoError {
|
||||
/// Create a read error
|
||||
pub fn read(path: impl Into<String>, source: impl ToString) -> Self {
|
||||
Self::Read(format!("{}: {}", path.into(), source.to_string()))
|
||||
}
|
||||
|
||||
/// Create a write error
|
||||
pub fn write(path: impl Into<String>, source: impl ToString) -> Self {
|
||||
Self::Write(format!("{}: {}", path.into(), source.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Installation error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InstallationError {
|
||||
/// Package not found
|
||||
#[error("package not found: {0}")]
|
||||
PackageNotFound(String),
|
||||
|
||||
/// Installation failed
|
||||
#[error("installation failed: {0}")]
|
||||
InstallationFailed(String),
|
||||
|
||||
/// Validation failed
|
||||
#[error("validation failed: {0}")]
|
||||
ValidationFailed(String),
|
||||
|
||||
/// Permission denied
|
||||
#[error("permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
}
|
||||
|
||||
/// Validation error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidationError {
|
||||
/// Invalid configuration
|
||||
#[error("invalid configuration: {0}")]
|
||||
InvalidConfiguration(String),
|
||||
|
||||
/// Missing dependency
|
||||
#[error("missing dependency: {0}")]
|
||||
MissingDependency(String),
|
||||
|
||||
/// Constraint violation
|
||||
#[error("constraint violation: {0}")]
|
||||
ConstraintViolation(String),
|
||||
}
|
||||
|
||||
// Convenience aliases
|
||||
pub type Result<T> = std::result::Result<T, AgentConfigError>;
|
||||
122
qiming-rcoder/crates/agent_config/src/types/installation.rs
Normal file
122
qiming-rcoder/crates/agent_config/src/types/installation.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
//! Installation configuration structures.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Package manager type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PackageManager {
|
||||
/// npm package manager
|
||||
Npm,
|
||||
/// Local binary
|
||||
Local,
|
||||
/// Custom package manager
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// Installation configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InstallationConfig {
|
||||
/// Package manager
|
||||
pub package_manager: PackageManager,
|
||||
|
||||
/// Package name
|
||||
#[serde(default)]
|
||||
pub package_name: Option<String>,
|
||||
|
||||
/// Version constraint
|
||||
#[serde(default)]
|
||||
pub version: Option<String>,
|
||||
|
||||
/// Installation source (for custom sources)
|
||||
#[serde(default)]
|
||||
pub source: Option<String>,
|
||||
|
||||
/// Validation command
|
||||
#[serde(default)]
|
||||
pub validate_command: Option<Vec<String>>,
|
||||
|
||||
/// Whether to auto-update
|
||||
#[serde(default)]
|
||||
pub auto_update: bool,
|
||||
}
|
||||
|
||||
impl Default for InstallationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
package_manager: PackageManager::Npm,
|
||||
package_name: None,
|
||||
version: None,
|
||||
source: None,
|
||||
validate_command: None,
|
||||
auto_update: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallationConfig {
|
||||
/// Create npm installation config
|
||||
pub fn npm(package_name: String) -> Self {
|
||||
Self {
|
||||
package_manager: PackageManager::Npm,
|
||||
package_name: Some(package_name),
|
||||
version: Some("latest".to_string()),
|
||||
source: None,
|
||||
validate_command: None,
|
||||
auto_update: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create local installation config
|
||||
pub fn local(command: String) -> Self {
|
||||
Self {
|
||||
package_manager: PackageManager::Local,
|
||||
package_name: Some(command),
|
||||
version: None,
|
||||
source: None,
|
||||
validate_command: None,
|
||||
auto_update: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create custom installation config
|
||||
pub fn custom(package_manager: String, source: String) -> Self {
|
||||
Self {
|
||||
package_manager: PackageManager::Custom(package_manager),
|
||||
package_name: None,
|
||||
version: None,
|
||||
source: Some(source),
|
||||
validate_command: None,
|
||||
auto_update: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set version
|
||||
pub fn with_version(mut self, version: String) -> Self {
|
||||
self.version = Some(version);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set validation command
|
||||
pub fn with_validation(mut self, command: Vec<String>) -> Self {
|
||||
self.validate_command = Some(command);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable auto-update
|
||||
pub fn with_auto_update(mut self) -> Self {
|
||||
self.auto_update = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager {
|
||||
/// Get the manager name as string
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
PackageManager::Npm => "npm",
|
||||
PackageManager::Local => "local",
|
||||
PackageManager::Custom(name) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
123
qiming-rcoder/crates/agent_config/src/types/mcp_config.rs
Normal file
123
qiming-rcoder/crates/agent_config/src/types/mcp_config.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! MCP server configuration structures.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
/// MCP server source type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum McpServerSource {
|
||||
/// Custom command (npm, uvx, bun, etc.)
|
||||
Custom,
|
||||
/// Local executable file
|
||||
Local,
|
||||
}
|
||||
|
||||
/// MCP server configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpServerConfig {
|
||||
/// Server name
|
||||
pub name: String,
|
||||
|
||||
/// Server source type
|
||||
pub source: McpServerSource,
|
||||
|
||||
/// Whether enabled
|
||||
pub enabled: bool,
|
||||
|
||||
/// Command to execute (for custom/local sources)
|
||||
pub command: Option<String>,
|
||||
|
||||
/// Command arguments
|
||||
pub args: Option<Vec<String>>,
|
||||
|
||||
/// Environment variables
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
|
||||
/// Connection timeout
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Context server configuration (simplified format for config files)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContextServerConfig {
|
||||
/// Server source type
|
||||
pub source: String,
|
||||
|
||||
/// Whether enabled
|
||||
pub enabled: bool,
|
||||
|
||||
/// Command to execute
|
||||
pub command: Option<String>,
|
||||
|
||||
/// Command arguments
|
||||
pub args: Option<Vec<String>>,
|
||||
|
||||
/// Environment variables
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl McpServerConfig {
|
||||
/// Create a new MCP server config
|
||||
pub fn new(name: String, source: McpServerSource) -> Self {
|
||||
Self {
|
||||
name,
|
||||
source,
|
||||
enabled: true,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
timeout: Some(Duration::from_secs(30)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a custom MCP server
|
||||
pub fn custom(name: String, command: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
source: McpServerSource::Custom,
|
||||
enabled: true,
|
||||
command: Some(command),
|
||||
args: None,
|
||||
env: None,
|
||||
timeout: Some(Duration::from_secs(30)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a local MCP server
|
||||
pub fn local(name: String, command: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
source: McpServerSource::Local,
|
||||
enabled: true,
|
||||
command: Some(command),
|
||||
args: None,
|
||||
env: None,
|
||||
timeout: Some(Duration::from_secs(30)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if server is enabled
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextServerConfig {
|
||||
/// Create a new context server config
|
||||
pub fn new(source: String) -> Self {
|
||||
Self {
|
||||
source,
|
||||
enabled: true,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if server is enabled
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
}
|
||||
19
qiming-rcoder/crates/agent_config/src/types/mod.rs
Normal file
19
qiming-rcoder/crates/agent_config/src/types/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! Core type definitions for agent configuration.
|
||||
|
||||
pub mod agent_config;
|
||||
pub mod agent_spec;
|
||||
pub mod error;
|
||||
pub mod installation;
|
||||
pub mod mcp_config;
|
||||
pub mod prompt_config;
|
||||
pub mod system_prompt;
|
||||
|
||||
pub use agent_config::*;
|
||||
pub use agent_spec::*;
|
||||
pub use error::*;
|
||||
pub use installation::*;
|
||||
pub use mcp_config::*;
|
||||
pub use prompt_config::*;
|
||||
pub use system_prompt::DEFAULT_SYSTEM_PROMPT;
|
||||
#[allow(deprecated)]
|
||||
pub use system_prompt::PromptBuilder;
|
||||
240
qiming-rcoder/crates/agent_config/src/types/prompt_config.rs
Normal file
240
qiming-rcoder/crates/agent_config/src/types/prompt_config.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
//! Prompt configuration structures.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::system_prompt::DEFAULT_SYSTEM_PROMPT;
|
||||
|
||||
/// 系统提示词来源
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SystemPromptSource {
|
||||
/// 使用编译时嵌入的默认提示词
|
||||
#[default]
|
||||
Embedded,
|
||||
/// 使用自定义模板内容
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// System prompt configuration
|
||||
///
|
||||
/// 支持两种模式:
|
||||
/// 1. `source: "embedded"` - 使用编译时嵌入的默认提示词(忽略 template 字段)
|
||||
/// 2. `source: "custom"` - 使用 template 字段的自定义内容
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemPromptConfig {
|
||||
/// 提示词来源
|
||||
/// - "embedded": 使用编译时嵌入的默认提示词
|
||||
/// - "custom": 使用 template 字段的自定义内容
|
||||
#[serde(default)]
|
||||
pub source: SystemPromptSource,
|
||||
|
||||
/// Template content (仅当 source = "custom" 时使用)
|
||||
#[serde(default)]
|
||||
pub template: String,
|
||||
|
||||
/// Whether enabled (false 时不传递系统提示词)
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// User prompt configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserPromptConfig {
|
||||
/// Template content with {user_prompt} placeholder
|
||||
pub template: String,
|
||||
|
||||
/// Whether enabled
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Default enabled value
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for SystemPromptConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
source: SystemPromptSource::Embedded,
|
||||
template: String::new(),
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemPromptConfig {
|
||||
/// 创建使用嵌入默认提示词的配置
|
||||
pub fn embedded() -> Self {
|
||||
Self {
|
||||
source: SystemPromptSource::Embedded,
|
||||
template: String::new(),
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建使用自定义模板的配置
|
||||
pub fn custom(template: String) -> Self {
|
||||
Self {
|
||||
source: SystemPromptSource::Custom,
|
||||
template,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建禁用的配置(不传递系统提示词)
|
||||
pub fn disabled() -> Self {
|
||||
Self {
|
||||
source: SystemPromptSource::Embedded,
|
||||
template: String::new(),
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取系统提示词内容
|
||||
///
|
||||
/// - 如果 enabled = false,返回 None
|
||||
/// - 如果 source = "embedded",返回编译时嵌入的默认提示词
|
||||
/// - 如果 source = "custom",返回 template 字段的内容
|
||||
pub fn get_prompt(&self) -> Option<&str> {
|
||||
if !self.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
match self.source {
|
||||
SystemPromptSource::Embedded => Some(DEFAULT_SYSTEM_PROMPT),
|
||||
SystemPromptSource::Custom => {
|
||||
if self.template.is_empty() {
|
||||
// custom 模式但 template 为空,回退到默认
|
||||
Some(DEFAULT_SYSTEM_PROMPT)
|
||||
} else {
|
||||
Some(&self.template)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取系统提示词内容(兼容旧接口,始终返回有效提示词)
|
||||
///
|
||||
/// 如果 enabled = false,仍返回默认提示词(用于需要始终有提示词的场景)
|
||||
pub fn get_prompt_or_default(&self) -> &str {
|
||||
match self.source {
|
||||
SystemPromptSource::Embedded => DEFAULT_SYSTEM_PROMPT,
|
||||
SystemPromptSource::Custom => {
|
||||
if self.template.is_empty() {
|
||||
DEFAULT_SYSTEM_PROMPT
|
||||
} else {
|
||||
&self.template
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否使用自定义模板
|
||||
pub fn is_custom(&self) -> bool {
|
||||
self.source == SystemPromptSource::Custom && !self.template.is_empty()
|
||||
}
|
||||
|
||||
/// 检查是否使用嵌入的默认提示词
|
||||
pub fn is_embedded(&self) -> bool {
|
||||
self.source == SystemPromptSource::Embedded
|
||||
}
|
||||
}
|
||||
|
||||
impl UserPromptConfig {
|
||||
/// Create a new user prompt config
|
||||
pub fn new(template: String) -> Self {
|
||||
Self {
|
||||
template,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create disabled config
|
||||
pub fn disabled() -> Self {
|
||||
Self {
|
||||
template: String::new(),
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply user prompt template
|
||||
pub fn apply(&self, user_input: &str) -> String {
|
||||
if self.enabled {
|
||||
self.template.replace("{user_prompt}", user_input)
|
||||
} else {
|
||||
user_input.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_embedded_source() {
|
||||
let config = SystemPromptConfig::embedded();
|
||||
assert!(config.is_embedded());
|
||||
assert!(!config.is_custom());
|
||||
assert!(config.get_prompt().is_some());
|
||||
assert!(
|
||||
config
|
||||
.get_prompt()
|
||||
.unwrap()
|
||||
.contains("<SYSTEM_INSTRUCTIONS>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_source() {
|
||||
let config = SystemPromptConfig::custom("Custom prompt".to_string());
|
||||
assert!(config.is_custom());
|
||||
assert!(!config.is_embedded());
|
||||
assert_eq!(config.get_prompt(), Some("Custom prompt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_config() {
|
||||
let config = SystemPromptConfig::disabled();
|
||||
assert!(config.get_prompt().is_none());
|
||||
// get_prompt_or_default 仍返回默认值
|
||||
assert!(
|
||||
config
|
||||
.get_prompt_or_default()
|
||||
.contains("<SYSTEM_INSTRUCTIONS>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_empty_template_fallback() {
|
||||
let config = SystemPromptConfig {
|
||||
source: SystemPromptSource::Custom,
|
||||
template: String::new(),
|
||||
enabled: true,
|
||||
};
|
||||
// 空模板时回退到默认
|
||||
assert!(
|
||||
config
|
||||
.get_prompt()
|
||||
.unwrap()
|
||||
.contains("<SYSTEM_INSTRUCTIONS>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_deserialization() {
|
||||
let json = r#"{"source": "embedded", "template": "", "enabled": true}"#;
|
||||
let config: SystemPromptConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(config.is_embedded());
|
||||
assert!(config.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_deserialization_custom() {
|
||||
let json = r#"{"source": "custom", "template": "My prompt", "enabled": true}"#;
|
||||
let config: SystemPromptConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(config.is_custom());
|
||||
assert_eq!(config.get_prompt(), Some("My prompt"));
|
||||
}
|
||||
}
|
||||
107
qiming-rcoder/crates/agent_config/src/types/system_prompt.rs
Normal file
107
qiming-rcoder/crates/agent_config/src/types/system_prompt.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
//! System prompt configuration types
|
||||
//!
|
||||
//! This module provides compile-time embedded default system prompt constants and related helper functions.
|
||||
//! Default system prompt is embedded at compile time from external files, with runtime override support via configuration.
|
||||
|
||||
/// Compile-time embedded default system prompt
|
||||
pub const DEFAULT_SYSTEM_PROMPT: &str = include_str!("../../configs/prompts/frontend_expert.txt");
|
||||
|
||||
/// Prompt builder (kept for compatibility, recommend using SystemPromptConfig::get_prompt() directly)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PromptBuilder;
|
||||
|
||||
impl PromptBuilder {
|
||||
/// Build user prompt (without system prompt)
|
||||
pub fn build_user_prompt(user_prompt: &str) -> String {
|
||||
user_prompt.to_string()
|
||||
}
|
||||
|
||||
/// Build user prompt with data sources
|
||||
pub fn build_user_prompt_with_data_sources(
|
||||
user_prompt: &str,
|
||||
data_sources: &[String],
|
||||
) -> String {
|
||||
if data_sources.is_empty() {
|
||||
return user_prompt.to_string();
|
||||
}
|
||||
|
||||
let data_sources_section = format_data_sources(data_sources);
|
||||
format!(
|
||||
"{}\n\n\
|
||||
<DATA_SOURCES>\n\
|
||||
The following are available data source information, including backend API endpoints, database connections, and other external data sources.\n\
|
||||
When developing frontend applications, you can use these data sources to fetch real data, such as querying Bitcoin transaction volumes, stock prices, weather information, etc.\n\
|
||||
Please use these data sources reasonably according to development needs, and ensure the frontend application can correctly call the relevant interfaces.\n\
|
||||
Use Axios client or Fetch API for API calls, or according to the interface calling method of the current framework.\n\n\
|
||||
{}\n\
|
||||
</DATA_SOURCES>",
|
||||
user_prompt, data_sources_section
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format data source information into readable text
|
||||
fn format_data_sources(data_sources: &[String]) -> String {
|
||||
if data_sources.is_empty() {
|
||||
return "No data sources".to_string();
|
||||
}
|
||||
|
||||
let mut formatted = String::new();
|
||||
|
||||
for (index, data_source) in data_sources.iter().enumerate() {
|
||||
formatted.push_str(&format!("Data source {}:\n", index + 1));
|
||||
|
||||
// 尝试解析 JSON 字符串并格式化
|
||||
match serde_json::from_str::<serde_json::Value>(data_source) {
|
||||
Ok(json_value) => {
|
||||
// 成功解析,格式化为易读的 JSON
|
||||
match serde_json::to_string_pretty(&json_value) {
|
||||
Ok(pretty_json) => {
|
||||
formatted.push_str(&pretty_json);
|
||||
}
|
||||
Err(_) => {
|
||||
// 格式化失败,使用原始字符串
|
||||
formatted.push_str(data_source);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// 不是有效的 JSON,直接使用原始字符串
|
||||
formatted.push_str(data_source);
|
||||
}
|
||||
}
|
||||
|
||||
formatted.push('\n');
|
||||
}
|
||||
|
||||
formatted
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_system_prompt_loaded() {
|
||||
// 验证默认提示词被正确加载
|
||||
assert!(!DEFAULT_SYSTEM_PROMPT.is_empty());
|
||||
assert!(DEFAULT_SYSTEM_PROMPT.contains("<SYSTEM_INSTRUCTIONS>"));
|
||||
assert!(DEFAULT_SYSTEM_PROMPT.contains("</SYSTEM_INSTRUCTIONS>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_data_sources() {
|
||||
let data_sources = vec![r#"{"api": "https://api.example.com"}"#.to_string()];
|
||||
let formatted = format_data_sources(&data_sources);
|
||||
|
||||
assert!(formatted.contains("Data source 1"));
|
||||
assert!(formatted.contains("api.example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_empty_data_sources() {
|
||||
let data_sources: Vec<String> = vec![];
|
||||
let formatted = format_data_sources(&data_sources);
|
||||
assert_eq!(formatted, "No data sources");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user