代码架构.md 9.3 KB

web server


# ./openhands/server/listen.py
# ./containers/app/Dockerfile
# 等同于 openhands-app:/app/openhands/frontend/build ./frontend/build

docker cp openhands-app:/app/openhands /home/mrh/program/openhands/testm
uvicorn openhands.server.listen:app --host 0.0.0.0 --port 3000

启动 runtime

启动了 web app , websocket_endpoint 利用 websocket 与控制是否启动 runtime 按浏览器会话来决定runtime任务,可能是为了让不同的客户端会话决定对同一个项目做不同的任务发起 event_stream = await session_manager.init_or_join_session

OpenHands/openhands/server/listen_socket.py

启动 agent_session await self.start_local_session(sid, session_init_data) - await session.initialize_agent(session_init_data)

OpenHands/openhands/server/session/manager.py await self.agent_session.start OpenHands/openhands/server/session/session.py self._start_thread, - await self._create_runtime OpenHands/openhands/server/session/agent_session.py

await self._create_runtimeruntime_cls = get_runtime_cls(runtime_name)

./openhands/server/session/agent_session.py

尝试连接到之前的容器,如果之前保留了会话,可根据容器名连接: async def connect(self): , await call_sync_from_async(self._attach_to_container) 如果容器不存在,则初始化一个新容器: await call_sync_from_async(self._init_container) 容器名来自前缀+sid self.container_name = CONTAINER_NAME_PREFIX + sid ,sid 来自 listen.py 会话或者 jwt_token sid = get_sid_from_token(jwt_token, config.jwt_secret) sid = str(uuid.uuid4())

./openhands/runtime/impl/docker/docker_runtime.py ./openhands/server/listen.py

在 runtime 内部,python -u -m openhands.runtime.action_execution_server 用 api server 来操作文件 添加所有插件 plugins_to_load.append(ALL_PLUGINS[plugin]()) 加载插件 (self._init_plugin(plugin) for plugin in self.plugins_to_load), 初始化插件 await plugin.initialize(self.username)

./openhands/runtime/action_execution_server.py

在 runtime 内部,初始化 vscode 插件。 f'exec /openhands/.openvscode-server/bin/openvscode-server --host 0.0.0.0 --connection-token {self.vscode_connection_token} --port {self.vscode_port}\n' port 地址是 port_mapping 在文件 eventstream_runtime.pyself.container = self.docker_client.containers.run 时传参数

./openhands/runtime/plugins/vscode/init.py

runtime 内部的初始化

self.container = self.docker_client.containers.run

OpenHands/openhands/runtime/action_execution_server.py

文件操作

  1. 前端通过 WebSocket 发送文件更新请求到 listen_socket.py

    • 请求通过 websocket_endpoint 接收
    • 消息格式包含操作类型(创建/修改/删除)和文件内容
    • 消息被路由到对应的 session handler
  2. session_manager.py 中处理请求

    • init_or_join_session 获取或创建对应会话
    • handle_message 方法解析消息类型
    • 文件操作请求被转发到 agent_session
  3. agent_session.py 处理文件操作

    • process_file_operation 方法处理具体操作
    • 创建/修改操作通过 write_file 方法
    • 删除操作通过 delete_file 方法
    • 所有操作通过 runtime 的 API 执行
  4. 文件操作通过 runtime 执行

    • 通过 action_execution_server.py 的 API 接口
    • 使用 FileOperationsPlugin 处理具体文件操作
    • 操作在容器内的 workspace 目录执行
    • 结果通过 WebSocket 返回给前端
  5. 文件同步机制

    • 使用 inotify 监控文件变化
    • 变化通过 FileWatcherPlugin 处理
    • 重要变化会通知前端更新
    • 双向同步确保一致性

vscode

route: http://sv-v2:3000/api/vscode-url

vscode 访问地址

# ./openhands/runtime/impl/docker/docker_runtime.py
# VSCODE_HOST=sv-v2
                vscode_host = os.environ.get('VSCODE_HOST', "localhost")
                self._vscode_url = f'http://{vscode_host}:{self._host_port + 1}/?tkn={response_json["token"]}&folder={self.config.workspace_mount_path_in_sandbox}'

vscode server ./openhands/runtime/utils/runtime_build.py ./openhands/runtime/utils/runtime_templates/Dockerfile.j2

cd ~/program/openhands/testm
docker run -it --init -p 9806:3000 -v "$(pwd):/home/workspace:cached" gitpod/openvscode-server

自定义 Runtime 镜像

文件修改

# 从 docker 容器 openhands-app 内复制到宿主机
docker cp openhands-app:/app/openhands/core/cli.py /home/mrh/program/openhands
docker cp openhands-app:/app/openhands/core/cli.py ./openhands/core/cli.py
# 切换到指定版本
git checkout 0.15.0
# 挂载
      - ./openhands/core/cli.py:/app/openhands/core/cli.py

python -m openhands.core.cli

# ./openhands/server/listen.py
# ./containers/app/Dockerfile
uvicorn openhands.server.listen:app --host 0.0.0.0 --port 3000

# eventstream_runtime.py:234

构建自己的 Runtime 镜像

🎈 手动构建

命令行构建说明: ./containers/runtime/README.md 模板: ./openhands/runtime/utils/runtime_templates/Dockerfile.j2 文档说明: ./docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/architecture/runtime.md 有关文件: openhands/runtime/utils/runtime_build.py

build_runtime_image 使用了构建器 runtime_builder=DockerRuntimeBuilder(docker.from_env()), _build_sandbox_image

openhands/runtime/utils/runtime_build.py 创建构建命令: buildx_cmd ./openhands/runtime/builder/docker.py

指定文件夹需要有 Dockerfile template 文件。在 buildx_cmd.append(path) # must be last! 中看到 path 是一个文件夹路径,构建时会自动在这个文件夹内查找 Dockerfile template 文件。 在 _generate_dockerfile 中看到会渲染 Dockerfile template 文件 根据已有 Dockerfile.j2 来渲染这个文件,也就是将文件中 {{base_image}} 等双花括号的内容通过 jijia 改成正常的 Dockerfile 文件。 渲染的结果 Dockerfile 文件会保存到 --build_folder 同等目录下,

./openhands/runtime/utils/runtime_templates/Dockerfile.j2

🎈 启动时,如不存在则自动构建

启动时,if self.runtime_container_image is None 会运行 EventStreamRuntime build_runtime_image 构建 runtime 镜像

./openhands/runtime/impl/docker/docker_runtime.py ./openhands/runtime/utils/runtime_build.py ./openhands/runtime/utils/runtime_templates/Dockerfile.j2

self.runtime_container_image = self.config.sandbox.runtime_container_image 取决于 config.toml 文件的配置

./openhands/core/config/app_config.py

config.toml 从这里来 load_app_config

./openhands/server/listen.py

Dockerfile.j2 解析

当你运行 runtime_build.py 后,调用 build_runtime_image_in_folder 函数。 base_imagebuild_fromextra_deps 需要作为函数传参 build_from 是根据你的基准镜像来决定构建方式,如果不是默认镜像 nikolaik/python-nodejs 则 Dockerfile.j2 会条件渲染,安装有关的 Python 环境,如果 build_from 使用了默认的 Python 环境,则 Dockerfile.j2 会跳过安装,进而检查版本。 extra_deps 似乎没有被用到,运行 py 脚本也没有这个传参。可能考虑未来如果需要额外安装别的软件,可以额外添加这个 shell 代码。

OpenHands/openhands/runtime/utils/runtime_build.py

{% macro setup_base_system() %} 代码块是 jijia 模板的宏定义,以 {% endmacro %} 结尾,宏定义内的代码可以由Python渲染。代码内实现了常用工具的安装:wget curl sudo apt-utils git

{% macro setup_vscode_server() %} 实现了 openvscode-server 的安装。 {% macro install_dependencies() %} 宏是实现了 openhands 项目中 Python requirement 等软件的安装 {% if build_from_scratch %} 代码内可以看到,它决定了是否调用 {{ setup_base_system() }} {{ setup_vscode_server() }} ,也就是 build_from_scratch 变量决定了根据本地镜像和版本,是否复用构建或强制重新构建。 这些镜像名称的不同区分主要是为了实现以下几个核心用途: | 镜像名称变量 | 实际镜像示例 | 用途 | |-----------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------| | base_image | nikolaik/python-nodejs:python3.12-nodejs22 | 基础镜像,包含操作系统和基础工具。 | | hash_image_name | docker.all-hands.dev/all-hands-ai/runtime:oh_v0.15_image_nikolaik_tag_python3.12-nodejs22 | 最终目标镜像,包含源代码和所有依赖项。 | | lock_image_name | docker.all-hands.dev/all-hands-ai/runtime:oh_v0.15_lock_nikolaik_python3.12-nodejs22 | 基于依赖锁定文件生成的镜像,包含依赖项。 | | versioned_image_name| docker.all-hands.dev/all-hands-ai/runtime:oh_v0.15_nikolaik_python3.12-nodejs22 | 基于基础镜像和 OpenHands 版本生成的镜像,包含基础环境和部分依赖项。 |

默认情况下是 build_from == BuildFromImageType.SCRATCH ,即 build_from_scratch == true ,build_from_versioned == false