# web server ```shell # ./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_runtime` ,`runtime_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/eventstream/eventstream_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.py`中 `self.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 ### 文件修改 ```shell # 从 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 ``` ## vscode route: http://sv-v2:3000/api/vscode-url ### vscode 访问地址 ```python # ./openhands/runtime/impl/eventstream/eventstream_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 ```shell cd ~/program/openhands/testm docker run -it --init -p 9806:3000 -v "$(pwd):/home/workspace:cached" gitpod/openvscode-server ``` ## 构建自己的 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/eventstream/eventstream_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_image` 、 `build_from` 、`extra_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