首页
/ OpenHands 前端 API 服务层实现指南:Service 模式、类型化客户端与 TanStack Query 集成

OpenHands 前端 API 服务层实现指南:Service 模式、类型化客户端与 TanStack Query 集成

2026-09-04 20:01:43作者:秋阔奎Evelyn

OpenHands(Agent Canvas)前端将「组件 → 后端 API」之间的网络访问统一收敛到 src/api/ 下的服务层:本地 agent-server 访问必须使用 @openhands/typescript-client 类型化客户端并复用共享连接选项,云端访问则走 cloud 代理模块。读完本篇,你将掌握该服务层的目录结构、命名规范、连接选项解析机制、强制架构守卫测试,以及如何用 TanStack Query hooks 正确消费这些服务。

1. 服务层的定位:组件与后端之间的唯一抽象层

根据 API Services Guide 的定义,服务(Services)是前端组件与后端 API 之间的抽象层。当前仓库中它遵循两条明确的通道划分:

  • 本地 agent-server 通道:直接使用 @openhands/typescript-client 导出的客户端类(如 LLMMetadataClientVSCodeClientServerClient),连接参数(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.tsmain-app-auth.ts 等,它们与目录化服务共同构成完整的 API 层。

当前实际落地的服务目录包括:config-service/conversation-service/event-service/git-service/mcp-service/secrets-service.tssettings-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.ts secrets-service.api.ts
类型文件 feature.types.ts secrets.types.ts
导出名 featureService secretsService

以仓库中真实存在的 config-service 为例,其目录内即为 config-service.api.ts + config-service.types.ts 的双文件结构,与规范完全一致。需要注意一个实现细节:部分真实服务(如 config-service.api.tsconversation-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) 的解析规则:

  1. host 解析优先级overrides.host > overrides.conversationUrl(经 buildHttpBaseUrl 从 WebSocket URL 推导 HTTP base)> 当前激活本地后端的 backend.host(来自 backend-registry/active-store.ts)。所有路径都会经 normalizeHost 去除尾部斜杠;
  2. 无后端时的失败语义:若既没有激活的本地 Backend,也没有任何 host/conversationUrl 覆盖,抛出类型化的 NoBackendAvailableError(附带 isNoBackendAvailableError 类型守卫),供上层区分「未配置后端」与网络错误;
  3. API key 优先级sessionApiKey > apiKey > backend.apiKey
  4. workingDir 默认值:取 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.tsBackend 接口携带 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 }): Promise<Feature> => {
    return new FeatureClient(getAgentServerClientOptions()).getFeature(id);
  },

  createFeature: async (params: CreateFeatureParams): Promise<Feature> => {
    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.tsapi/cloud/proxy.tsapi/main-app-auth.ts。这也解释了为什么云端通道必须走 cloud/proxy.tscallCloudProxy——它是全仓库唯一合法的通用 HTTP 出口之一。

5. 云端通道:callCloudProxy 与本地路径的分流

cloud/proxy.ts 中的 callCloudProxy<TResponse>(req) 接收 CloudProxyRequestbackend(必须为 cloud 类型的 Backend)、methodpath、可选 body/headers/timeoutSeconds/hostOverride,以及认证模式(bearer 默认 / session-api-key / none)。内部通过 createCloudClient / createCloudClientForRuntime 构造云客户端再转发请求。

真实服务如何二选一分流,可以见 config-service.api.tssearchModels

const 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 callCloudProxy<LLMModelPage>({
    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 hooks

  • Caching —— 避免冗余网络请求
  • Deduplication —— 多个组件请求同一数据时共享一次请求
  • Loading/error states —— 内置 isLoadingisErrordata 状态
  • Background refetching —— 数据自动保持新鲜

Hooks 的位置约定:

  • src/hooks/query/:数据获取(useQuery
  • src/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 能力时,按以下顺序操作即可与仓库规范保持一致:

  1. 确认端点是否存在于 @openhands/typescript-client;若缺失,先给该 SDK 补端点,而不是在本仓库手写 HTTP(守卫测试 no-direct-agent-server-calls.test.ts 会拦截后者);
  2. src/api/<feature>-service/ 下创建 <feature>-service.api.ts<feature>.types.ts,导出命名对象 <feature>Service,方法内通过 getAgentServerClientOptions() 构造类型化客户端;
  3. 若该能力仅存在于云端,改经 callCloudProxy(参考 config-service.api.tskind === "cloud" 分流写法);
  4. src/hooks/query/src/hooks/mutation/ 中为它包一层 TanStack Query hook,组件只消费 hook;
  5. 需要特殊行为(会话级密钥、超时、workingDir 覆盖)时,通过 AgentServerClientOverrides 传入覆盖项,而不是绕过共享连接选项自行拼 URL。

这套「目录即模块、SDK 即客户端、hook 即消费口」的分层,是 OpenHands 前端在本地与云端双后端架构下保持 API 访问可测试、可缓存、可审计的核心工程约定。

登录后查看全文
热门项目推荐
相关项目推荐