动手实践OpenHands系列学习笔记17:构建自定义OpenHands应用

笔记17:构建自定义OpenHands应用

一、引言

OpenHands作为可扩展的AI驱动软件开发代理平台,不仅提供了丰富的内置功能,还允许开发者构建自定义应用和扩展。通过基于OpenHands的核心能力,开发者可以创建针对特定领域或工作流的专用AI代理应用。本笔记将探讨OpenHands的可扩展架构,分析自定义应用的设计模式,并通过实践构建一个专门的代码重构助手应用。

二、OpenHands扩展性架构

2.1 扩展点与接口设计

OpenHands的可扩展架构主要包括以下核心扩展点:

  • 代理能力扩展: 定制代理的技能和工具集
  • 工具集成接口: 连接外部工具和服务的标准化接口
  • UI扩展: 定制用户界面和交互流程
  • 后端服务扩展: 增强或替换核心处理逻辑
  • 持久化扩展: 自定义数据存储和检索机制
  • 安全与权限扩展: 定制访问控制和安全策略

2.2 应用类型与设计模式

  1. 专用工具型应用:

    • 聚焦特定开发任务的专用工具
    • 简化界面和交互流程
    • 强调单一功能的深度优化
  2. 工作流集成应用:

    • 嵌入现有开发工作流
    • 与其他开发工具协同工作
    • 自动化重复任务
  3. 领域特化应用:

    • 针对特定技术栈或领域优化
    • 内置领域特定知识和最佳实践
    • 定制化提示和工具链
  4. 企业定制应用:

    • 集成企业内部系统和工具
    • 符合企业安全和合规要求
    • 适应企业特有开发流程

2.3 扩展架构关键技术

  • 插件系统: 支持动态加载和管理扩展
  • 配置驱动设计: 通过配置灵活调整行为
  • 事件总线: 组件间松耦合通信机制
  • 适配器模式: 连接不同系统和接口
  • 中间件链: 可扩展的请求处理流程
  • 模板引擎: 自定义提示和输出格式

三、自定义应用开发流程

3.1 需求分析与应用规划

根据README_CN.md中的信息,OpenHands支持多种运行方式和配置选项,这为自定义应用提供了基础:

  1. 明确应用目标:

    • 定义要解决的具体问题
    • 确定目标用户和使用场景
    • 设定成功指标
  2. 功能范围规划:

    • 核心功能列表
    • 必要的OpenHands能力
    • 额外需要开发的组件
  3. 技术架构设计:

    • 选择适当的集成方式(API、容器、插件等)
    • 确定前端交互模式
    • 规划数据流和持久化需求

3.2 OpenHands集成方案

基于README_CN.md中描述的OpenHands部署选项,自定义应用可以通过以下方式与OpenHands集成:

  1. Docker容器化集成:

    docker run -it --rm --pull=always \
      -e CUSTOM_APP_CONFIG=/app/config.json \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v ~/.openhands:/.openhands \
      -v $(pwd):/workspace \
      -p 3000:3000 \
      my-custom-openhands-app:latest
    
  2. API驱动集成:

    • 利用OpenHands无头模式API
    • 构建自定义前端与OpenHands后端通信
    • 实现特定领域的控制逻辑
  3. 插件式扩展:

    • 开发符合OpenHands插件规范的模块
    • 注册自定义工具和能力
    • 在运行时动态加载

四、实践:构建代码重构助手应用

4.1 应用设计与架构

我们将构建一个专注于代码重构的自定义OpenHands应用,具有以下特点:

  • 专门针对代码重构任务优化
  • 提供常见重构模式的模板
  • 支持重构前后的代码比较
  • 集成单元测试验证重构结果
  • 保存重构历史以便回顾和学习

应用架构包括:

  1. OpenHands核心作为后端引擎
  2. 自定义前端界面
  3. 重构模式库
  4. 代码分析和比较组件
  5. 测试自动化集成

4.2 关键组件实现

4.2.1 自定义配置和启动脚本
#!/usr/bin/env python3
# refactor_assistant.py
# 基于OpenHands的代码重构助手应用

import os
import sys
import json
import subprocess
import argparse
from pathlib import Path

# 配置
CONFIG_DIR = Path.home() / ".refactor-assistant"
CONFIG_FILE = CONFIG_DIR / "config.json"
OPENHANDS_CONFIG = {
    "model": "anthropic/claude-sonnet-4-20250514",
    "tools": ["codebase_search", "read_file", "edit_file", "run_terminal_cmd"],
    "systemPrompt": """你是一位代码重构专家,擅长使用设计模式和现代编程实践改进代码质量。
你的任务是分析代码并提出重构建议,然后帮助实施这些改进。
关注以下几点:
1. 代码可读性和可维护性
2. 设计模式的恰当应用
3. 解耦和单一职责原则
4. 测试覆盖和可测试性
5. 性能优化机会

始终保留代码的功能等价性,确保重构前后行为一致。"""
}

def ensure_config():
    """确保配置目录和文件存在"""
    CONFIG_DIR.mkdir(exist_ok=True)
    if not CONFIG_FILE.exists():
        default_config = {
            "api_key": "",
            "editor": "code",  # 默认使用VS Code
            "workspace": "",
            "refactor_patterns": {
                "extract_method": "将这段代码提取为一个独立的方法,提高可读性和复用性",
                "replace_conditional": "使用多态替代这些条件判断",
                "introduce_parameter": "引入参数替代硬编码值",
                "extract_class": "将相关功能提取到专门的类中"
            }
        }
        with open(CONFIG_FILE, "w") as f:
            json.dump(default_config, f, indent=2)
    return load_config()

def load_config():
    """加载配置"""
    with open(CONFIG_FILE, "r") as f:
        return json.load(f)

def start_openhands(config):
    """启动定制化的OpenHands实例"""
    # 准备环境变量
    env = os.environ.copy()
    if config.get("api_key"):
        env["ANTHROPIC_API_KEY"] = config["api_key"]
    
    # 准备OpenHands配置
    temp_config = Path(CONFIG_DIR) / "openhands_config.json"
    with open(temp_config, "w") as f:
        json.dump(OPENHANDS_CONFIG, f)
    
    # 构建Docker命令
    workspace = config.get("workspace") or os.getcwd()
    cmd = [
        "docker", "run", "-it", "--rm", "--pull=always",
        "-e", f"SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik",
        "-e", "LOG_ALL_EVENTS=true",
        "-e", f"CONFIG_FILE=/.openhands/openhands_config.json",
        "-v", "/var/run/docker.sock:/var/run/docker.sock",
        "-v", f"{CONFIG_DIR}:/.openhands",
        "-v", f"{workspace}:/workspace",
        "-p", "3000:3000",
        "--add-host", "host.docker.internal:host-gateway",
        "--name", "refactor-assistant",
        "docker.all-hands.dev/all-hands-ai/openhands:0.47"
    ]
    
    print(f"启动重构助手...\n工作目录: {workspace}")
    subprocess.run(cmd, env=env)

def show_refactor_patterns(config):
    """显示可用的重构模式"""
    print("可用的重构模式:")
    patterns = config.get("refactor_patterns", {})
    for i, (name, desc) in enumerate(patterns.items(), 1):
        print(f"{i}. {name}: {desc}")

def add_refactor_pattern(config, name, description):
    """添加新的重构模式"""
    if "refactor_patterns" not in config:
        config["refactor_patterns"] = {}
    
    config["refactor_patterns"][name] = description
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, indent=2)
    print(f"已添加重构模式: {name}")

def main():
    """主函数"""
    parser = argparse.ArgumentParser(description="代码重构助手 - 基于OpenHands")
    subparsers = parser.add_subparsers(dest="command", help="可用命令")
    
    # 开始命令
    start_parser = subparsers.add_parser("start", help="启动重构助手")
    
    # 配置命令
    config_parser = subparsers.add_parser("config", help="配置应用")
    config_parser.add_argument("key", help="配置键")
    config_parser.add_argument("value", help="配置值")
    
    # 显示重构模式
    patterns_parser = subparsers.add_parser("patterns", help="显示可用的重构模式")
    
    # 添加重构模式
    add_pattern_parser = subparsers.add_parser("add-pattern", help="添加重构模式")
    add_pattern_parser.add_argument("name", help="模式名称")
    add_pattern_parser.add_argument("description", help="模式描述")
    
    args = parser.parse_args()
    config = ensure_config()
    
    if args.command == "start":
        start_openhands(config)
    elif args.command == "config":
        config[args.key] = args.value
        with open(CONFIG_FILE, "w") as f:
            json.dump(config, f, indent=2)
        print(f"已更新配置: {args.key} = {args.value}")
    elif args.command == "patterns":
        show_refactor_patterns(config)
    elif args.command == "add-pattern":
        add_refactor_pattern(config, args.name, args.description)
    else:
        parser.print_help()

if __name__ == "__main__":
    main()
4.2.2 重构助手前端界面

为了提供专门的重构体验,我们可以创建一个简单的HTML界面,与OpenHands API集成:


DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>代码重构助手title>
    <style>
        :root {
            --primary: #007ACC;
            --bg-dark: #1E1E1E;
            --bg-light: #252526;
            --text: #E8E8E8;
            --border: #3C3C3C;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 0;
            background-color: var(--bg-dark);
            color: var(--text);
            height: 100vh;
            display: flex;
            flex-direction: column;
        }
        
        header {
            background-color: var(--bg-light);
            padding: 10px 20px;
            border-bottom: 1px solid var(--border);
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        
        main {
            display: flex;
            flex-grow: 1;
            overflow: hidden;
        }
        
        .sidebar {
            width: 250px;
            background-color: var(--bg-light);
            border-right: 1px solid var(--border);
            padding: 10px;
            overflow-y: auto;
        }
        
        .content {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            padding: 10px;
            overflow: hidden;
        }
        
        .editor-container {
            display: flex;
            flex-grow: 1;
            overflow: hidden;
            margin-bottom: 10px;
        }
        
        .editor {
            flex: 1;
            border: 1px solid var(--border);
            margin: 0 5px;
            overflow: auto;
            background-color: var(--bg-light);
        }
        
        .chat-container {
            height: 200px;
            border: 1px solid var(--border);
            background-color: var(--bg-light);
            display: flex;
            flex-direction: column;
        }
        
        .chat-messages {
            flex-grow: 1;
            padding: 10px;
            overflow-y: auto;
        }
        
        .chat-input {
            display: flex;
            border-top: 1px solid var(--border);
            padding: 10px;
        }
        
        input, select, button, textarea {
            background-color: var(--bg-dark);
            color: var(--text);
            border: 1px solid var(--border);
            padding: 8px;
            font-family: inherit;
        }
        
        button {
            background-color: var(--primary);
            cursor: pointer;
            border: none;
        }
        
        button:hover {
            opacity: 0.9;
        }
        
        textarea {
            flex-grow: 1;
            resize: none;
        }
        
        h3 {
            margin-top: 10px;
            margin-bottom: 8px;
        }
        
        ul {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        
        li {
            padding: 8px 10px;
            cursor: pointer;
            border-radius: 4px;
        }
        
        li:hover {
            background-color: rgba(255, 255, 255, 0.1);
        }
        
        .active {
            background-color: rgba(0, 122, 204, 0.3);
        }
        
        .diff-added {
            background-color: rgba(40, 167, 69, 0.2);
        }
        
        .diff-removed {
            background-color: rgba(220, 53, 69, 0.2);
        }
    style>
head>
<body>
    <header>
        <h2>代码重构助手h2>
        <div>
            <select id="pattern-select">
                <option value="">选择重构模式...option>
                
            select>
            <button id="run-tests">运行测试button>
        div>
    header>
    
    <main>
        <div class="sidebar">
            <h3>项目文件h3>
            <ul id="file-list">
                
            ul>
            
            <h3>重构历史h3>
            <ul id="history-list">
                
            ul>
        div>
        
        <div class="content">
            <div class="editor-container">
                <div class="editor" id="original-code">
                    
                    <pre>// 选择文件查看代码pre>
                div>
                <div class="editor" id="refactored-code">
                    
                    <pre>// 重构后的代码将显示在这里pre>
                div>
            div>
            
            <div class="chat-container">
                <div class="chat-messages" id="chat-messages">
                    
                    <div class="message system">欢迎使用代码重构助手!请选择文件并描述您想要的重构。div>
                div>
                <div class="chat-input">
                    <textarea id="message-input" placeholder="描述要进行的重构...">textarea>
                    <button id="send-button">发送button>
                div>
            div>
        div>
    main>

    <script>
        // API基础URL
        const API_BASE = 'http://localhost:3000/api';
        let currentSession = null;
        let currentFile = null;
        
        // 初始化函数
        async function init() {
            // 加载重构模式
            await loadRefactorPatterns();
            
            // 加载项目文件
            await loadProjectFiles();
            
            // 创建会话
            await createSession();
            
            // 事件监听
            document.getElementById('send-button').addEventListener('click', sendMessage);
            document.getElementById('pattern-select').addEventListener('change', applyPattern);
            document.getElementById('run-tests').addEventListener('click', runTests);
        }
        
        // 加载重构模式
        async function loadRefactorPatterns() {
            try {
                const response = await fetch('/.openhands/config.json');
                const config = await response.json();
                const patterns = config.refactor_patterns || {};
                
                const select = document.getElementById('pattern-select');
                Object.entries(patterns).forEach(([name, desc]) => {
                    const option = document.createElement('option');
                    option.value = name;
                    option.textContent = name;
                    option.title = desc;
                    select.appendChild(option);
                });
            } catch (error) {
                console.error('加载重构模式失败:', error);
            }
        }
        
        // 加载项目文件
        async function loadProjectFiles() {
            try {
                const response = await fetch(`${API_BASE}/files`);
                const files = await response.json();
                
                const fileList = document.getElementById('file-list');
                files.forEach(file => {
                    if (file.type === 'file') {
                        const li = document.createElement('li');
                        li.textContent = file.name;
                        li.addEventListener('click', () => loadFile(file.path));
                        fileList.appendChild(li);
                    }
                });
            } catch (error) {
                console.error('加载项目文件失败:', error);
                addMessage('系统', '加载项目文件失败,请确保API服务正在运行。');
            }
        }
        
        // 创建会话
        async function createSession() {
            try {
                const response = await fetch(`${API_BASE}/sessions`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        model: 'anthropic/claude-sonnet-4-20250514'
                    })
                });
                const data = await response.json();
                currentSession = data.sessionId;
                console.log('会话已创建:', currentSession);
            } catch (error) {
                console.error('创建会话失败:', error);
                addMessage('系统', '创建会话失败,请检查OpenHands服务状态。');
            }
        }
        
        // 加载文件
        async function loadFile(path) {
            try {
                const response = await fetch(`${API_BASE}/files/content?path=${encodeURIComponent(path)}`);
                const content = await response.text();
                
                document.getElementById('original-code').innerHTML = `
${escapeHtml(content)}
`
; currentFile = path; // 更新UI状态 document.querySelectorAll('#file-list li').forEach(li => { li.classList.remove('active'); if (li.textContent === path.split('/').pop()) { li.classList.add('active'); } }); addMessage('系统', `已加载文件: ${path}`); } catch (error) { console.error('加载文件失败:', error); addMessage('系统', '加载文件失败,请重试。'); } } // 发送消息 async function sendMessage() { const input = document.getElementById('message-input'); const message = input.value.trim(); if (!message || !currentSession) return; addMessage('用户', message); input.value = ''; try { const fileContext = currentFile ? `当前文件: ${currentFile}\n` : ''; const fullMessage = `${fileContext}${message}`; const response = await fetch(`${API_BASE}/sessions/${currentSession}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: fullMessage }) }); const data = await response.json(); const messageId = data.messageId; // 等待响应 await waitForResponse(messageId); } catch (error) { console.error('发送消息失败:', error); addMessage('系统', '发送消息失败,请重试。'); } } // 等待响应 async function waitForResponse(messageId) { try { while (true) { const response = await fetch(`${API_BASE}/sessions/${currentSession}/messages/${messageId}`); const data = await response.json(); if (data.status === 'completed') { addMessage('助手', data.response); // 检查是否有文件更改 if (currentFile) { loadFile(currentFile); } break; } else if (data.status === 'error') { addMessage('系统', `错误: ${data.error || '未知错误'}`); break; } // 等待一段时间再查询 await new Promise(resolve => setTimeout(resolve, 1000)); } } catch (error) { console.error('等待响应失败:', error); addMessage('系统', '等待响应失败,请重试。'); } } // 应用重构模式 function applyPattern() { const select = document.getElementById('pattern-select'); const pattern = select.options[select.selectedIndex]; if (!pattern.value || !currentFile) return; const message = `请使用"${pattern.value}"模式重构当前文件。 ${pattern.title}`; document.getElementById('message-input').value = message; sendMessage(); } // 运行测试 async function runTests() { if (!currentFile) { addMessage('系统', '请先选择一个文件。'); return; } addMessage('系统', '正在运行相关测试...'); try { const message = `请运行与${currentFile}相关的测试,验证重构是否保持了功能等价性。`; const response = await fetch(`${API_BASE}/sessions/${currentSession}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: message }) }); const data = await response.json(); await waitForResponse(data.messageId); } catch (error) { console.error('运行测试失败:', error); addMessage('系统', '运行测试失败,请重试。'); } } // 添加消息到聊天区域 function addMessage(sender, content) { const messages = document.getElementById('chat-messages'); const messageDiv = document.createElement('div'); messageDiv.className = `message ${sender.toLowerCase()}`; const senderSpan = document.createElement('strong'); senderSpan.textContent = sender + ': '; const contentSpan = document.createElement('span'); contentSpan.textContent = content; messageDiv.appendChild(senderSpan); messageDiv.appendChild(contentSpan); messages.appendChild(messageDiv); // 滚动到底部 messages.scrollTop = messages.scrollHeight; } // HTML转义 function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // 初始化应用 document.addEventListener('DOMContentLoaded', init);
script> body> html>

4.3 应用部署和使用指南

要部署并使用这个定制的代码重构助手:

  1. 设置环境:

    • 安装Docker
    • 保存上述脚本为refactor_assistant.py并添加执行权限
    • 保存UI文件为refactor_ui.html
  2. 配置应用:

    # 设置API密钥
    ./refactor_assistant.py config api_key sk-your-api-key
    
    # 设置工作空间
    ./refactor_assistant.py config workspace /path/to/your/project
    
    # 查看可用重构模式
    ./refactor_assistant.py patterns
    
    # 添加自定义重构模式
    ./refactor_assistant.py add-pattern state_pattern "将条件逻辑转换为状态模式,使状态转换更明确"
    
  3. 启动应用:

    ./refactor_assistant.py start
    
  4. 使用流程:

    • 在浏览器中访问http://localhost:3000
    • 选择要重构的源代码文件
    • 选择预定义的重构模式或自定义重构说明
    • 查看重构建议和实施结果
    • 运行测试验证重构效果
    • 保存满意的重构结果

五、总结

构建自定义OpenHands应用是扩展AI代理能力的有效方式,可以针对特定领域或工作流创建专门化的工具。通过OpenHands的灵活架构,开发者可以在保留核心AI能力的同时,定制用户体验和功能集合,打造更适合特定场景的智能助手。

无论是专注于代码重构、架构审查、自动化测试还是其他开发任务,构建在OpenHands基础上的自定义应用都能充分利用其强大的代理能力和灵活的扩展性。随着对特定领域知识和工作流的深入优化,这些自定义应用可以成为开发团队的专属工具,显著提升特定场景下的开发效率和代码质量。

你可能感兴趣的:(动手实践OpenHands系列学习笔记17:构建自定义OpenHands应用)