OpenHands 前端 API 服务层实现指南Service 模式、类型化客户端与 TanStack Query 集成【免费下载链接】OpenHands OpenHands: AI-Driven Development项目地址: https://gitcode.com/GitHub_Trending/ope/OpenHandsOpenHandsAgent Canvas前端将「组件 → 后端 API」之间的网络访问统一收敛到src/api/下的服务层本地 agent-server 访问必须使用openhands/typescript-client类型化客户端并复用共享连接选项云端访问则走 cloud 代理模块。读完本篇你将掌握该服务层的目录结构、命名规范、连接选项解析机制、强制架构守卫测试以及如何用 TanStack Query hooks 正确消费这些服务。1. 服务层的定位组件与后端之间的唯一抽象层根据 API Services Guide 的定义服务Services是前端组件与后端 API 之间的抽象层。当前仓库中它遵循两条明确的通道划分本地 agent-server 通道直接使用openhands/typescript-client导出的客户端类如LLMMetadataClient、VSCodeClient、ServerClient连接参数host、session API key、workspace 默认值统一来自 agent-server-client-options.ts云端通道使用src/api/cloud/下的 cloud service 模块或代理 helper而不是本地 agent-server 客户端。每条服务以「纯对象 异步方法」的形式组织Each service is a plain object with async methods不引入类实例、单例或全局状态。从源码结构看src/api/下还包含一批不属于「feature-service 目录」形态的横切模块例如 agent-server-adapter.ts负责把 agent-server 的 wire 数据适配为前端AppConversation模型、with-retry.ts、main-app-auth.ts 等它们与目录化服务共同构成完整的 API 层。当前实际落地的服务目录包括config-service/、conversation-service/、event-service/、git-service/、mcp-service/、secrets-service.ts、settings-service/、workspaces-service/、bash-service/、automation-service/、option-service/、profiles-service/、provider-connections-service/、runtime-service/、agent-profiles-service/等全部位于 src/api/ 目录下。2. 目录结构与命名规范官方约定每个服务拥有独立目录且目录内固定两个文件src/api/ └── feature-service/ ├── feature-service.api.ts # Service methods └── feature.types.ts # Types and interfaces对应的命名规范如下完整继承自原文档项目规范示例目录feature-service/secrets-service/服务文件feature-service.api.tssecrets-service.api.ts类型文件feature.types.tssecrets.types.ts导出名featureServicesecretsService以仓库中真实存在的 config-service 为例其目录内即为config-service.api.tsconfig-service.types.ts的双文件结构与规范完全一致。需要注意一个实现细节部分真实服务如 config-service.api.ts、conversation-service.api.ts采用了带静态方法的类 export default的组织方式而规范推荐的是对象字面量 命名导出从源码结构看这是历史形态与现行约定的并存新代码应遵循 README 中的对象字面量写法。3. 连接选项解析getAgentServerClientOptions 的完整机制规范示例中出现的getAgentServerClientOptions()是整个服务层的地基其实现位于 agent-server-client-options.ts。完整参数与行为如下// src/api/agent-server-client-options.ts export interface AgentServerClientOverrides { host?: string; // 显式覆盖 host apiKey?: string | null; // 覆盖 API key sessionApiKey?: string | null; // 会话级 API key优先级最高 workingDir?: string; // 覆盖工作目录 conversationUrl?: string | null; // 由会话 URL 反推 host timeout?: number; // 请求超时毫秒 } export interface AgentServerClientOptions { host: string; apiKey?: string; workingDir: string; timeout?: number; }getAgentServerClientOptions(overrides)的解析规则host 解析优先级overrides.hostoverrides.conversationUrl经buildHttpBaseUrl从 WebSocket URL 推导 HTTP base 当前激活本地后端的backend.host来自 backend-registry/active-store.ts。所有路径都会经normalizeHost去除尾部斜杠无后端时的失败语义若既没有激活的本地 Backend也没有任何 host/conversationUrl 覆盖抛出类型化的NoBackendAvailableError附带isNoBackendAvailableError类型守卫供上层区分「未配置后端」与网络错误API key 优先级sessionApiKeyapiKeybackend.apiKeyworkingDir 默认值取getAgentServerWorkingDir()其默认常量DEFAULT_WORKING_DIR workspace/project定义在 agent-server-config.ts 中。此外还有一个 HTTP 变体getAgentServerHttpClientOptions()它把上述选项转换为 SDK 的baseUrl/apiKey/timeout形态且timeout 默认 60000ms。例如 conversation-service.api.ts 中拉取轨迹数据时就用了这个变体const page await new RemoteEventsList( getAgentServerHttpClientOptions(this.getClientOverrides()), conversationId, ).search({ limit: 10000 });其中getClientOverrides()会注入当前会话的session_api_key体现了「会话级密钥优先」的设计——这与 backend-registry/types.ts 中Backend接口携带apiKey/connectionRevision字段的注册表机制相互衔接。4. 创建一个新服务规范写法与关键约束原文档给出的创建范式值得逐条拆解使用对象字面量 命名导出参数用对象解构使调用自文档化优先使用类型化的openhands/typescript-client类而非通用 HTTP 调用如果缺少所需端点先给openhands/typescript-client补齐而不是在应用内手写 fetch。// feature-service/feature-service.api.ts import { FeatureClient } from openhands/typescript-client/clients; import { getAgentServerClientOptions } from ../agent-server-client-options; import { Feature, CreateFeatureParams } from ./feature.types; export const featureService { getFeature: async ({ id }: { id: string }): PromiseFeature { return new FeatureClient(getAgentServerClientOptions()).getFeature(id); }, createFeature: async (params: CreateFeatureParams): PromiseFeature { return new FeatureClient(getAgentServerClientOptions()).createFeature(params); }, };// feature-service/feature.types.ts // 仅当 SDK 模型不足时才在独立类型文件中定义应用级类型 export interface Feature { id: string; name: string; description: string; } export interface CreateFeatureParams { name: string; description: string; }要点每个方法内部new XxxClient(getAgentServerClientOptions())即时构造客户端避免长生命周期实例参数对象解构{ id }: { id: string }让调用点形如featureService.getFeature({ id: abc })语义自明应用特有类型放在同目录feature.types.ts中与 SDK 模型解耦。类型化访问的强制守卫这条「不许手写 HTTP」的约定不是口号而是被测试强制执行的。no-direct-agent-server-calls.test.ts 会递归扫描src/下所有非测试的 TS/TSX 文件拦截以下行为直接使用共享 axios 实例openHands.xxx直接调用createHttpClient(...)或直接 import SDK 低层HttpClient直接new HttpClient(...)直接使用axios发起请求直接用fetch请求/api/路径。仅三个文件被显式加入白名单api/automation-service/automation-service.api.ts、api/cloud/proxy.ts、api/main-app-auth.ts。这也解释了为什么云端通道必须走 cloud/proxy.ts 的callCloudProxy——它是全仓库唯一合法的通用 HTTP 出口之一。5. 云端通道callCloudProxy 与本地路径的分流cloud/proxy.ts 中的callCloudProxyTResponse(req)接收CloudProxyRequestbackend必须为 cloud 类型的 Backend、method、path、可选body/headers/timeoutSeconds/hostOverride以及认证模式bearer默认 /session-api-key/none。内部通过createCloudClient/createCloudClientForRuntime构造云客户端再转发请求。真实服务如何二选一分流可以见 config-service.api.ts 的searchModelsconst active getActiveBackend(); if (active.backend.kind cloud) { // 云端直接暴露 /api/v1/config/models/search返回 LLMModelPage const qs buildCloudQueryString({ page_id: params.page_id, limit: params.limit, query: params.query, verified__eq: params.verified__eq, provider__eq: params.provider__eq, }); return callCloudProxyLLMModelPage({ backend: active.backend, method: GET, path: /api/v1/config/models/search${qs}, }); } // 本地走类型化 LLMMetadataClient并在此做 verified 状态重建与分页/过滤 const llmClient new LLMMetadataClient(getAgentServerClientOptions()); const [models, verifiedMap] await Promise.all([ llmClient.getModels(), llmClient.getVerifiedModels(), ]); // ...过滤 query / verified__eq / limit 后返回 { items, next_page_id: null }该实现同时展示了本地路径的一个实用模式Promise.all并行拉取模型列表与 verified 模型映射支持外部传入预取的verifiedByProvider以避免重复请求并在客户端侧完成query/verified__eq/limit三个过滤参数的完整落地——即文档所说的「参数说明可执行」。6. 消费方式必须包在 TanStack Query hooks 中原文档用 IMPORTANT 级别强调不要在组件里直接调用 service一律包成 TanStack Query hooksCaching—— 避免冗余网络请求Deduplication—— 多个组件请求同一数据时共享一次请求Loading/error states—— 内置isLoading、isError、data状态Background refetching—— 数据自动保持新鲜Hooks 的位置约定src/hooks/query/数据获取useQuerysrc/hooks/mutation/写入/更新useMutation标准示例原文档示例路径前缀#/为仓库内 alias// src/hooks/query/use-feature.ts import { useQuery } from tanstack/react-query; import { featureService } from #/api/feature-service/feature-service.api; export const useFeature (id: string) { return useQuery({ queryKey: [feature, id], queryFn: () featureService.getFeature({ id }), }); };这与仓库实际结构吻合src/hooks/query/ 下约 70 余个 query hooks 与 src/hooks/mutation/ 下约 40 余个 mutation hooks一一对应src/api/中的服务模块。组件层因此只依赖 hook 的data/isLoading/error永远不感知 host、API key 或请求重试细节。7. 实践清单新增一个前端 API 能力时按以下顺序操作即可与仓库规范保持一致确认端点是否存在于openhands/typescript-client若缺失先给该 SDK 补端点而不是在本仓库手写 HTTP守卫测试 no-direct-agent-server-calls.test.ts 会拦截后者在src/api/feature-service/下创建feature-service.api.ts与feature.types.ts导出命名对象featureService方法内通过getAgentServerClientOptions()构造类型化客户端若该能力仅存在于云端改经callCloudProxy参考 config-service.api.ts 的kind cloud分流写法在src/hooks/query/或src/hooks/mutation/中为它包一层 TanStack Query hook组件只消费 hook需要特殊行为会话级密钥、超时、workingDir 覆盖时通过AgentServerClientOverrides传入覆盖项而不是绕过共享连接选项自行拼 URL。这套「目录即模块、SDK 即客户端、hook 即消费口」的分层是 OpenHands 前端在本地与云端双后端架构下保持 API 访问可测试、可缓存、可审计的核心工程约定。【免费下载链接】OpenHands OpenHands: AI-Driven Development项目地址: https://gitcode.com/GitHub_Trending/ope/OpenHands创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考