独家合作CubenceAI 中转平台立减 20%访问

开发文档

扩展开发

扩展开发

Claude Code Hub 采用模块化设计,提供多个扩展点供二次开发。本文档介绍如何扩展系统的各个核心组件。


扩展点概述

CCH 提供以下主要扩展点:

扩展点位置用途
供应商类型src/types/provider.ts添加新的 AI 服务供应商类型
格式转换器src/app/v1/_lib/converters/支持新的 API 格式转换
Guard Pipelinesrc/app/v1/_lib/proxy/guard-pipeline.ts添加新的请求处理守卫
Server Actionssrc/actions/添加新的后台管理 API
UI 组件src/app/[locale]/扩展管理界面功能
环境变量src/lib/config/env.schema.ts添加新的配置项

添加新供应商类型

当需要支持新的 AI 服务供应商(如自建模型服务)时,需要修改多个文件。

步骤 1:修改类型定义

编辑 src/types/provider.ts,添加新的供应商类型:

// src/types/provider.ts
export type ProviderType =
  | "claude"
  | "claude-auth"
  | "codex"
  | "gemini"
  | "gemini-cli"
  | "openai-compatible"
  | "your-new-type";  // 添加新类型

步骤 2:更新数据库 Schema

编辑 src/drizzle/schema.ts,更新 providerType 字段的类型约束:

// src/drizzle/schema.ts
providerType: varchar('provider_type', { length: 20 })
  .notNull()
  .default('claude')
  .$type<'claude' | 'claude-auth' | 'codex' | 'gemini-cli' | 'gemini' | 'openai-compatible' | 'your-new-type'>(),

数据库迁移

修改 Schema 后需要生成并执行数据库迁移:

bun run db:generate
bun run db:migrate

步骤 3:添加格式映射

如果新供应商使用独特的 API 格式,需要在 src/app/v1/_lib/proxy/format-mapper.ts 中添加映射:

// src/app/v1/_lib/proxy/format-mapper.ts

// 1. 添加到 ClientFormat 类型
export type ClientFormat = "response" | "openai" | "claude" | "gemini" | "gemini-cli" | "your-format";

// 2. 添加端点检测规则
const endpointPatterns: Array<{ pattern: RegExp; format: ClientFormat }> = [
  // ... 现有规则
  { pattern: /^\/v1\/your-endpoint$/i, format: "your-format" },
];

// 3. 添加格式转换映射
export function mapClientFormatToTransformer(clientFormat: ClientFormat): Format {
  switch (clientFormat) {
    // ... 现有映射
    case "your-format":
      return "your-new-type";
    // ...
  }
}

步骤 4:更新 UI 表单

编辑供应商表单组件,添加新类型的选项:

// src/app/[locale]/settings/providers/_components/forms/provider-form.tsx

const PROVIDER_TYPES = [
  { value: "claude", label: "Claude (Anthropic)" },
  { value: "claude-auth", label: "Claude 中转服务" },
  { value: "codex", label: "Codex CLI" },
  { value: "gemini-cli", label: "Gemini CLI" },
  { value: "openai-compatible", label: "OpenAI Compatible" },
  { value: "your-new-type", label: "您的新类型" },  // 添加
];

步骤 5:添加测试预设(可选)

如果新供应商需要特定的测试逻辑,编辑 src/lib/provider-testing/presets.ts

// src/lib/provider-testing/presets.ts
export function getPresetsForProvider(providerType: ProviderType): PresetConfig[] {
  switch (providerType) {
    // ... 现有预设
    case "your-new-type":
      return [
        {
          name: "基础测试",
          endpoint: "/v1/your-endpoint",
          // ... 测试配置
        },
      ];
  }
}

添加新格式转换器

格式转换器负责在不同 API 格式之间转换请求和响应。

转换器接口

转换器需要实现以下接口:

// src/app/v1/_lib/converters/types.ts

// API 格式类型
export type Format = "claude" | "codex" | "gemini-cli" | "openai-compatible";

// 请求转换函数
export type RequestTransform = (
  model: string,
  rawJSON: Record<string, unknown>,
  stream: boolean
) => Record<string, unknown>;

// 流式响应转换函数
export type ResponseStreamTransform = (
  ctx: Context,
  model: string,
  originalRequest: Record<string, unknown>,
  transformedRequest: Record<string, unknown>,
  chunk: string,
  state?: TransformState
) => string[];

// 非流式响应转换函数
export type ResponseNonStreamTransform = (
  ctx: Context,
  model: string,
  originalRequest: Record<string, unknown>,
  transformedRequest: Record<string, unknown>,
  response: Record<string, unknown>
) => Record<string, unknown>;

// 响应转换器
export interface ResponseTransform {
  stream?: ResponseStreamTransform;
  nonStream?: ResponseNonStreamTransform;
}

创建转换器模块

your-formatclaude 的转换为例:

src/app/v1/_lib/converters/your-format-to-claude/
├── index.ts      # 注册入口
├── request.ts    # 请求转换逻辑
└── response.ts   # 响应转换逻辑

请求转换器

// src/app/v1/_lib/converters/your-format-to-claude/request.ts
import { logger } from "@/lib/logger";

export function transformYourFormatRequestToClaude(
  model: string,
  rawJSON: Record<string, unknown>,
  stream: boolean
): Record<string, unknown> {
  logger.debug("[YourFormat→Claude] Transforming request", { model, stream });

  // 转换请求格式
  const claudeRequest = {
    model,
    max_tokens: rawJSON.max_tokens ?? 4096,
    messages: transformMessages(rawJSON.messages as any[]),
    stream,
    // ... 其他字段转换
  };

  return claudeRequest;
}

function transformMessages(messages: any[]): any[] {
  // 实现消息格式转换
  return messages.map(msg => ({
    role: msg.role,
    content: msg.content,
  }));
}

响应转换器

// src/app/v1/_lib/converters/your-format-to-claude/response.ts
import type { Context } from "hono";
import type { TransformState } from "../types";
import { logger } from "@/lib/logger";

export function transformClaudeStreamResponseToYourFormat(
  ctx: Context,
  model: string,
  originalRequest: Record<string, unknown>,
  transformedRequest: Record<string, unknown>,
  chunk: string,
  state?: TransformState
): string[] {
  // 解析 SSE chunk
  if (!chunk.startsWith("data: ")) {
    return [chunk];
  }

  const data = chunk.slice(6);
  if (data === "[DONE]") {
    return ["data: [DONE]\n\n"];
  }

  try {
    const parsed = JSON.parse(data);
    // 转换响应格式
    const transformed = {
      // ... 转换逻辑
    };
    return [`data: ${JSON.stringify(transformed)}\n\n`];
  } catch (error) {
    logger.warn("[Claude→YourFormat] Failed to parse chunk", { error });
    return [chunk];
  }
}

export function transformClaudeNonStreamResponseToYourFormat(
  ctx: Context,
  model: string,
  originalRequest: Record<string, unknown>,
  transformedRequest: Record<string, unknown>,
  response: Record<string, unknown>
): Record<string, unknown> {
  // 转换非流式响应
  return {
    // ... 转换逻辑
  };
}

注册转换器

// src/app/v1/_lib/converters/your-format-to-claude/index.ts
import { registerTransformer } from "../registry";
import { transformYourFormatRequestToClaude } from "./request";
import {
  transformClaudeStreamResponseToYourFormat,
  transformClaudeNonStreamResponseToYourFormat,
} from "./response";

// 注册 YourFormat → Claude 转换器
registerTransformer("your-format", "claude", transformYourFormatRequestToClaude, {
  stream: transformClaudeStreamResponseToYourFormat,
  nonStream: transformClaudeNonStreamResponseToYourFormat,
});

激活转换器

src/app/v1/_lib/converters/index.ts 中导入新转换器:

// src/app/v1/_lib/converters/index.ts

// 导入转换器(副作用:自动注册到 defaultRegistry)
import "./codex-to-claude";
import "./openai-to-claude";
// ... 现有导入
import "./your-format-to-claude";  // 添加新转换器

扩展 Guard Pipeline

Guard Pipeline 是请求处理的核心链路,用于认证、限流、供应商选择等。

Guard 接口

每个 Guard 需要实现以下接口:

// src/app/v1/_lib/proxy/guard-pipeline.ts

export interface GuardStep {
  name: string;
  execute(session: ProxySession): Promise<Response | null>;
}
  • 返回 null:继续执行下一个 Guard
  • 返回 Response:提前终止 Pipeline,返回该响应

创建新 Guard

// src/app/v1/_lib/proxy/your-guard.ts
import type { ProxySession } from "./session";
import { ProxyResponses } from "./responses";
import { logger } from "@/lib/logger";

export class YourGuard {
  static async ensure(session: ProxySession): Promise<Response | null> {
    logger.debug("[YourGuard] Checking request", {
      userId: session.authState?.user?.id,
    });

    // 执行检查逻辑
    const isValid = await yourValidationLogic(session);

    if (!isValid) {
      // 返回错误响应,终止 Pipeline
      return ProxyResponses.buildError(403, "检查未通过");
    }

    // 检查通过,继续下一个 Guard
    return null;
  }
}

async function yourValidationLogic(session: ProxySession): Promise<boolean> {
  // 实现自定义验证逻辑
  return true;
}

注册 Guard

guard-pipeline.ts 中注册新 Guard:

// src/app/v1/_lib/proxy/guard-pipeline.ts
import { YourGuard } from "./your-guard";

// 1. 添加 Guard Key
export type GuardStepKey =
  | "auth"
  | "version"
  | "probe"
  | "session"
  | "sensitive"
  | "rateLimit"
  | "provider"
  | "messageContext"
  | "yourGuard";  // 添加

// 2. 注册 Guard 实现
const Steps: Record<GuardStepKey, GuardStep> = {
  // ... 现有 Guards
  yourGuard: {
    name: "yourGuard",
    async execute(session) {
      return YourGuard.ensure(session);
    },
  },
};

// 3. 添加到 Pipeline 配置
export const CHAT_PIPELINE: GuardConfig = {
  steps: [
    "auth",
    "version",
    "probe",
    "session",
    "sensitive",
    "yourGuard",  // 在适当位置添加
    "rateLimit",
    "provider",
    "messageContext",
  ],
};

Guard 顺序

Guard 的执行顺序很重要。通常建议:

  1. 认证 Guard 在最前面
  2. 限流 Guard 在供应商选择之前
  3. 日志记录 Guard 在最后

添加新 Server Action

Server Actions 提供后台管理 API,自动生成 OpenAPI 文档。

Action 结构

每个 Action 模块遵循以下结构:

// src/actions/your-module.ts
"use server";

import { getSession } from "@/lib/auth";
import { logger } from "@/lib/logger";
import type { ActionResult } from "./types";

/**
 * 获取数据列表
 */
export async function getYourDataList(): Promise<ActionResult<YourData[]>> {
  try {
    const session = await getSession();
    if (!session || session.user.role !== "admin") {
      return { success: false, error: "权限不足" };
    }

    const data = await fetchYourData();
    return { success: true, data };
  } catch (error) {
    logger.error("获取数据失败:", error);
    return { success: false, error: "获取数据失败" };
  }
}

/**
 * 创建数据
 */
export async function createYourData(
  input: CreateYourDataInput
): Promise<ActionResult<YourData>> {
  try {
    const session = await getSession();
    if (!session || session.user.role !== "admin") {
      return { success: false, error: "权限不足" };
    }

    // 验证输入
    const validated = CreateYourDataSchema.safeParse(input);
    if (!validated.success) {
      return { success: false, error: validated.error.message };
    }

    const data = await insertYourData(validated.data);
    return { success: true, data };
  } catch (error) {
    logger.error("创建数据失败:", error);
    return { success: false, error: "创建数据失败" };
  }
}

Zod Schema 定义

src/lib/validation/schemas.ts 中定义验证 Schema:

// src/lib/validation/schemas.ts
import { z } from "zod";

export const CreateYourDataSchema = z.object({
  name: z.string().min(1, "名称不能为空").max(64, "名称不能超过64个字符"),
  description: z.string().max(200, "描述不能超过200个字符").optional(),
  isEnabled: z.boolean().default(true),
  config: z.record(z.unknown()).optional(),
});

export const UpdateYourDataSchema = CreateYourDataSchema.partial();

export type CreateYourDataInput = z.infer<typeof CreateYourDataSchema>;
export type UpdateYourDataInput = z.infer<typeof UpdateYourDataSchema>;

注册 OpenAPI 端点

src/app/api/actions/[...route]/route.ts 中注册 Action:

// src/app/api/actions/[...route]/route.ts
import * as yourModuleActions from "@/actions/your-module";
import { CreateYourDataSchema, UpdateYourDataSchema } from "@/lib/validation/schemas";

// 获取列表
const { route: getYourDataListRoute, handler: getYourDataListHandler } = createActionRoute(
  "your-module",
  "getYourDataList",
  yourModuleActions.getYourDataList,
  {
    description: "获取数据列表 (管理员)",
    tags: ["您的模块"],
    requiredRole: "admin",
  }
);
app.openapi(getYourDataListRoute, getYourDataListHandler);

// 创建数据
const { route: createYourDataRoute, handler: createYourDataHandler } = createActionRoute(
  "your-module",
  "createYourData",
  yourModuleActions.createYourData,
  {
    requestSchema: CreateYourDataSchema,
    description: "创建数据 (管理员)",
    tags: ["您的模块"],
    requiredRole: "admin",
  }
);
app.openapi(createYourDataRoute, createYourDataHandler);

// 更新文档标签
app.doc("/openapi.json", {
  // ...
  tags: [
    // ... 现有标签
    { name: "您的模块", description: "您的模块管理" },
  ],
});

扩展 UI 功能

添加新页面

创建新的管理页面:

src/app/[locale]/settings/your-page/
├── page.tsx              # 页面组件
└── _components/          # 页面专用组件
    ├── your-list.tsx
    └── your-form.tsx
// src/app/[locale]/settings/your-page/page.tsx
import { getTranslations } from "next-intl/server";
import { YourList } from "./_components/your-list";

export default async function YourPage() {
  const t = await getTranslations("yourPage");

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">{t("title")}</h1>
      </div>
      <YourList />
    </div>
  );
}

组件开发

使用项目的 UI 组件库:

// src/app/[locale]/settings/your-page/_components/your-form.tsx
"use client";

import { useState } from "react";
import { Button, Form, Input, message } from "antd";
import { createYourData } from "@/actions/your-module";

export function YourForm() {
  const [form] = Form.useForm();
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (values: any) => {
    setLoading(true);
    try {
      const result = await createYourData(values);
      if (result.success) {
        message.success("创建成功");
        form.resetFields();
      } else {
        message.error(result.error);
      }
    } finally {
      setLoading(false);
    }
  };

  return (
    <Form form={form} onFinish={handleSubmit} layout="vertical">
      <Form.Item
        name="name"
        label="名称"
        rules={[{ required: true, message: "请输入名称" }]}
      >
        <Input placeholder="输入名称" />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit" loading={loading}>
          提交
        </Button>
      </Form.Item>
    </Form>
  );
}

国际化

添加翻译文件:

// messages/zh-CN/yourPage.json
{
  "title": "您的页面",
  "create": "创建",
  "edit": "编辑",
  "delete": "删除",
  "name": "名称",
  "description": "描述",
  "confirmDelete": "确定要删除吗?"
}
// messages/en-US/yourPage.json
{
  "title": "Your Page",
  "create": "Create",
  "edit": "Edit",
  "delete": "Delete",
  "name": "Name",
  "description": "Description",
  "confirmDelete": "Are you sure you want to delete?"
}

在组件中使用:

import { useTranslations } from "next-intl";

export function YourComponent() {
  const t = useTranslations("yourPage");

  return <h1>{t("title")}</h1>;
}

添加新环境变量

定义 Schema

src/lib/config/env.schema.ts 中添加新变量:

// src/lib/config/env.schema.ts
import { z } from "zod";

const booleanTransform = (s: string) => s !== "false" && s !== "0";

export const EnvSchema = z.object({
  // ... 现有变量

  // 新增变量
  YOUR_NEW_VAR: z.string().optional(),
  YOUR_BOOLEAN_VAR: z.string().default("false").transform(booleanTransform),
  YOUR_NUMBER_VAR: z.coerce.number().default(100),
});

export type EnvConfig = z.infer<typeof EnvSchema>;

布尔值处理

不要使用 z.coerce.boolean(),因为 Boolean("false") === true 使用 transform 显式处理 "false""0" 字符串。

使用环境变量

import { getEnvConfig } from "@/lib/config/env.schema";

export function yourFunction() {
  const env = getEnvConfig();

  if (env.YOUR_BOOLEAN_VAR) {
    // 功能启用时的逻辑
  }

  const limit = env.YOUR_NUMBER_VAR;
  // ...
}

更新文档

.env.example 中添加示例:

# 您的新功能配置
YOUR_NEW_VAR=example-value
YOUR_BOOLEAN_VAR=true
YOUR_NUMBER_VAR=100

扩展最佳实践

代码组织

  • 遵循现有的目录结构和命名规范
  • 相关文件放在同一目录下
  • 使用 _components 目录存放页面专用组件
  • 公共组件放在 src/components 目录

类型安全

  • 所有新代码必须使用 TypeScript
  • 定义明确的接口和类型
  • 使用 Zod 进行运行时验证
  • 避免使用 any 类型

错误处理

// 推荐的错误处理模式
export async function yourAction(): Promise<ActionResult<Data>> {
  try {
    // 业务逻辑
    return { success: true, data };
  } catch (error) {
    logger.error("操作失败:", {
      error: error instanceof Error ? error.message : String(error),
      stack: error instanceof Error ? error.stack : undefined,
    });
    return { success: false, error: "操作失败,请稍后重试" };
  }
}

日志记录

import { logger } from "@/lib/logger";

// 使用适当的日志级别
logger.trace("详细调试信息");
logger.debug("调试信息");
logger.info("一般信息");
logger.warn("警告信息");
logger.error("错误信息", { error });

测试

  • 为新功能编写单元测试
  • 格式转换器需要覆盖流式和非流式场景
  • Guard 需要测试正常和异常情况
// 测试示例
import { describe, it, expect } from "bun:test";
import { transformYourFormatRequestToClaude } from "./request";

describe("YourFormat to Claude Request Transform", () => {
  it("should transform basic request", () => {
    const input = { /* ... */ };
    const result = transformYourFormatRequestToClaude("model", input, false);
    expect(result.model).toBe("model");
    // ...
  });
});

相关资源

Previous
贡献指南