【openclaw】OpenClaw Flows 模块超深度架构分析

OpenClaw Flows 模块超深度架构分析

分析版本:2026-04-20 | 代码目录:src/flows/ | 风格:Dark Terminal | 源码行数:2,430 行(9 文件,不含测试)


一、模块定位

1.1 业务职责

src/flows/ 是 OpenClaw 的交互式配置向导引擎(Interactive Setup Wizard Engine)。它将用户引导式设置流程抽象为"Flow Contribution"体系,统一管理渠道设置、Provider 配置、搜索配置、模型选择和健康诊断五大核心场景。业务职责精确定义为:

  1. Flow 抽象层types.ts)--- 定义 FlowContribution 统一贡献模型,实现跨模块可插拔的选项注册与合并
  2. Provider 设置流provider-flow.ts)--- Provider 向导选项与 Model Picker 条目的 Flow Contribution 构建
  3. Doctor 健康流doctor-health.ts + doctor-health-contributions.ts)--- 27 项健康检查的编排引擎,实现 openclaw doctor 命令
  4. 渠道设置流channel-setup.ts + channel-setup.status.ts + channel-setup.prompts.ts)--- 多渠道(Discord/Telegram/WhatsApp 等)交互式配置向导
  5. 搜索设置流search-setup.ts)--- Web Search Provider 选择、API Key 管理、Secret Input 模式
  6. 模型选择流model-picker.ts)--- 模型目录浏览、Provider 过滤、认证检查、Allowlist 管理

1.2 在系统中的位置

复制代码
┌────────────────────────────────────────────────────────────┐
│                      CLI Commands                           │
│   openclaw setup / openclaw doctor / openclaw configure     │
│                         ↓ 调用                               │
├────────────────────────────────────────────────────────────┤
│                    flows/ (本模块)                           │
│  ┌──────────┐ ┌──────────────┐ ┌───────────┐ ┌──────────┐ │
│  │ channel  │ │ doctor-health│ │  search   │ │  model   │ │
│  │  setup   │ │ contributions│ │  setup    │ │  picker  │ │
│  └────┬─────┘ └──────┬───────┘ └─────┬─────┘ └────┬─────┘ │
│       │              │               │             │        │
│  ┌────┴──────────────┴───────────────┴─────────────┴─────┐ │
│  │              types.ts (FlowContribution)              │ │
│  └──────────────────────┬────────────────────────────────┘ │
├─────────────────────────┼──────────────────────────────────┤
│          plugins / channels / commands / config             │
│              (业务基础设施,被 flows 调用)                   │
└────────────────────────────────────────────────────────────┘

flows/ 位于命令层与业务层之间 ,是用户交互式配置的编排中枢。它不直接实现业务逻辑,而是将 commands/channels/plugins/config/ 等模块的能力组合成连贯的向导流程。

1.3 核心业务价值

价值维度 具体体现 量化指标
统一抽象 FlowContribution 体系让 channel/provider/search/doctor 共享选项注册与合并机制 4 种 kind × 4 种 surface
可扩展 新 Provider/Channel 只需注册 FlowContribution,无需修改向导代码 Provider Plugin 自动出现
有序编排 Doctor 27 项检查按依赖顺序执行,前序结果传递给后续步骤 健康检查无遗漏
用户友好 交互式选择器 + 快速开始默认值 + 自动检测已配置项 降低新用户上手门槛
安全 Secret Input 模式(ref vs plaintext)、DM Policy 引导、Gateway Auth 检查 5+ 安全相关向导步骤
幂等性 重复运行 doctor/setup 不破坏已有配置,仅修复/建议 安全可重复执行

二、模块整体结构

2.1 文件清单与职责矩阵

文件 行数 导出数 职责
types.ts 58 7 Flow 基础类型 + Contribution 合并/排序工具函数
provider-flow.ts 156 7 Provider Setup + Model Picker 的 Flow Contribution 构建
doctor-health.ts 65 1 Doctor 命令入口(编排 27 项检查的执行顺序)
doctor-health-contributions.ts 637 5 Doctor 27 项健康检查的具体实现 + Contribution 注册
channel-setup.ts 602 7 渠道设置主流程(向导循环 + 配置修改 + 插件加载)
channel-setup.status.ts 267 7 渠道状态收集 + Contribution 构建 + QuickStart 默认值
channel-setup.prompts.ts 157 4 渠道设置专用提示(DM Policy / 已配置操作 / 账户选择)
search-setup.ts 542 10 搜索 Provider 设置流程(选择 + API Key + Secret Ref)
model-picker.ts 767 6 模型选择器(目录浏览 + Provider 过滤 + Allowlist)

总计:2,430 行有效代码(不含测试文件的 426 行)。

2.2 核心类型体系(types.ts 逐行解析)

typescript 复制代码
export type FlowDocsLink = {
  path: string;       // 文档路径(URL 或本地路径)
  label?: string;     // 可选的显示标签
};

设计目的 :为 Flow 选项提供文档链接。path 必填,label 可选(因为 UI 可以从 path 自动生成显示文本)。

FlowContributionKind
typescript 复制代码
export type FlowContributionKind = "channel" | "core" | "provider" | "search";

四种贡献类型

Kind 含义 代表模块
"channel" 渠道设置贡献 channel-setup
"core" 核心功能贡献(如 Doctor 健康检查) doctor-health
"provider" Provider 配置贡献 provider-flow
"search" 搜索引擎贡献 search-setup

设计目的 :区分不同维度的贡献,允许消费者按 kind 过滤。例如 model-picker 只关心 kind: "provider" 的贡献。

FlowContributionSurface
typescript 复制代码
export type FlowContributionSurface = "auth-choice" | "health" | "model-picker" | "setup";

四种展示面

Surface 含义 使用场景
"auth-choice" 认证选择 Provider 认证方式选择器
"health" 健康检查 Doctor 健康流程
"model-picker" 模型选择器 模型目录浏览
"setup" 初始设置 Provider/Channel/Search 初始配置

Kind × Surface 矩阵

auth-choice health model-picker setup
channel - - -
core - - -
provider - -
search - - -
FlowOptionGroup
typescript 复制代码
export type FlowOptionGroup = {
  id: string;       // 分组标识(如 Provider ID)
  label: string;    // 分组显示名
  hint?: string;    // 分组提示
};

设计目的:将选项按 Provider 分组。例如所有 OpenAI 模型归为一组,所有 Anthropic 模型归为另一组。

FlowOption
typescript 复制代码
export type FlowOption<Value extends string = string> = {
  value: Value;                          // 选项值(如 "openai/gpt-4o")
  label: string;                         // 显示名
  hint?: string;                         // 提示(如 "ctx 128K · reasoning")
  group?: FlowOptionGroup;               // 所属分组
  docs?: FlowDocsLink;                   // 文档链接
  assistantPriority?: number;            // AI 助手推荐优先级
  assistantVisibility?: "visible" | "manual-only";  // AI 可见性
};

泛型设计Value extends string = string 允许特定模块使用精确字面量类型(如 FlowOption<"openai" | "anthropic">),同时默认为宽泛 string。

assistantPriority / assistantVisibility :这两个字段控制 AI Agent 在自主设置时如何选择选项。priority 高的优先推荐,manual-only 的只在用户手动查看时显示。

FlowContribution
typescript 复制代码
export type FlowContribution<Value extends string = string> = {
  id: string;                           // 全局唯一标识(如 "provider:setup:openai")
  kind: FlowContributionKind;           // 贡献类型
  surface: FlowContributionSurface;     // 展示面
  option: FlowOption<Value>;            // 选项详情
  source?: string;                      // 来源(如 "runtime"、"doctor"、"catalog")
};

设计目的FlowContribution 是整个 Flow 体系的核心抽象。它将"某个模块可以向设置向导贡献一个选项"这个概念统一建模。每个贡献都有唯一 ID、类型、展示面、选项和来源。

mergeFlowContributions
typescript 复制代码
export function mergeFlowContributions<T extends FlowContribution>(params: {
  primary: readonly T[];      // 优先列表
  fallbacks?: readonly T[];   // 回退列表
}): T[] {
  const contributionByValue = new Map<string, T>();
  // Step 1: 优先列表全部注册
  for (const contribution of params.primary) {
    contributionByValue.set(contribution.option.value, contribution);
  }
  // Step 2: 回退列表仅注册 value 不重复的
  for (const contribution of params.fallbacks ?? []) {
    if (!contributionByValue.has(contribution.option.value)) {
      contributionByValue.set(contribution.option.value, contribution);
    }
  }
  return [...contributionByValue.values()];
}

合并策略 :primary 优先,fallbacks 补充。相同 option.value 的贡献,primary 版本胜出。这确保了内置选项不被外部覆盖,同时允许外部选项补充缺失项。

Map 选择 :使用 Map<string, T> 而非 Record<string, T>,因为 Map 保留插入顺序且键可以是任意字符串。

sortFlowContributionsByLabel
typescript 复制代码
export function sortFlowContributionsByLabel<T extends FlowContribution>(
  contributions: readonly T[],
): T[] {
  return [...contributions].toSorted(
    (left, right) =>
      left.option.label.localeCompare(right.option.label) ||
      left.option.value.localeCompare(right.option.value),
  );
}

排序策略 :先按 label 字母排序,label 相同时按 value 排序。使用 localeCompare 确保国际化排序正确。toSorted() 返回新数组,不修改原数组。


2.3 Provider Flow(provider-flow.ts 逐行解析)

模块职责:将 Plugin Provider 系统的 Wizard Options 和 Model Picker Entries 转换为标准 FlowContribution 格式。

类型定义
typescript 复制代码
// Provider 流的作用域
export type ProviderFlowScope = "text-inference" | "image-generation";
const DEFAULT_PROVIDER_FLOW_SCOPE: ProviderFlowScope = "text-inference";

设计目的:区分 Provider 是用于文本推理还是图像生成。默认为 text-inference,因为大多数 Provider 支持文本。

typescript 复制代码
// Provider Setup 的选项(扩展 FlowOption)
export type ProviderSetupFlowOption = FlowOption & {
  onboardingScopes?: ProviderFlowScope[];  // 该选项支持的作用域
};

// Provider Model Picker 的条目(等同 FlowOption)
export type ProviderModelPickerFlowEntry = FlowOption;
typescript 复制代码
// Provider Setup 的 FlowContribution
export type ProviderSetupFlowContribution = FlowContribution & {
  kind: "provider";           // 固定为 provider
  surface: "setup";           // 固定为 setup
  providerId: string;         // Provider 标识
  pluginId?: string;          // 所属插件标识
  option: ProviderSetupFlowOption;
  onboardingScopes?: ProviderFlowScope[];
  source: "runtime";          // 固定为 runtime(来自运行时插件系统)
};

// Provider Model Picker 的 FlowContribution
export type ProviderModelPickerFlowContribution = FlowContribution & {
  kind: "provider";
  surface: "model-picker";
  providerId: string;
  option: ProviderModelPickerFlowEntry;
  source: "runtime";
};

设计模式 :使用 TypeScript 的交集类型 & 扩展基类型,为特定 kind/surface 添加固定字段。kind: "provider" as const 确保类型收窄。

includesProviderFlowScope
typescript 复制代码
function includesProviderFlowScope(
  scopes: readonly ProviderFlowScope[] | undefined,
  scope: ProviderFlowScope,
): boolean {
  return scopes ? scopes.includes(scope) : scope === DEFAULT_PROVIDER_FLOW_SCOPE;
}

逻辑 :如果选项指定了 scopes,检查 scope 是否在其中;如果未指定 scopes,默认只匹配 "text-inference"(默认作用域)。这是空值即默认的设计模式。

resolveProviderDocsById
typescript 复制代码
function resolveProviderDocsById(params?: {
  config?: OpenClawConfig;
  workspaceDir?: string;
  env?: NodeJS.ProcessEnv;
}): Map<string, string> {
  return new Map(
    resolvePluginProviders({ ...params, mode: "setup" })
      .filter((provider): provider is ProviderPlugin & { docsPath: string } =>
        Boolean(normalizeOptionalString(provider.docsPath)),
      )
      .map((provider) => [provider.id, normalizeOptionalString(provider.docsPath)!]),
  );
}

逐行解析

  1. resolvePluginProviders({ mode: "setup" }) --- 以 setup 模式加载所有插件 Provider
  2. .filter() --- 类型守卫过滤出有 docsPath 的 Provider,同时将 docsPathstring | undefined 收窄为 string
  3. .map() --- 将每个 Provider 映射为 [id, docsPath] 元组
  4. new Map() --- 构建 id → docsPath 的映射表

设计目的:将 Provider 文档路径缓存为 Map,避免在构建每个 Contribution 时重复查找。

resolveProviderSetupFlowContributions
typescript 复制代码
export function resolveProviderSetupFlowContributions(params?: { ... }): ProviderSetupFlowContribution[] {
  const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;  // Step 1: 确定作用域
  const docsByProvider = resolveProviderDocsById(params ?? {}); // Step 2: 加载文档映射
  return sortFlowContributionsByLabel(                          // Step 4: 按 label 排序
    resolveProviderWizardOptions(params ?? {})                   // Step 3: 获取向导选项
      .filter((option) => includesProviderFlowScope(option.onboardingScopes, scope))
      .map((option) => ({                                        // Step 5: 映射为 Contribution
        id: `provider:setup:${option.value}`,
        kind: "provider" as const,
        surface: "setup" as const,
        providerId: option.groupId,
        option: {
          value: option.value,
          label: option.label,
          ...(option.hint ? { hint: option.hint } : {}),
          ...(option.assistantPriority !== undefined ? { assistantPriority: option.assistantPriority } : {}),
          ...(option.assistantVisibility ? { assistantVisibility: option.assistantVisibility } : {}),
          group: { id: option.groupId, label: option.groupLabel, ...(option.groupHint ? { hint: option.groupHint } : {}) },
          ...(docsByProvider.get(option.groupId) ? { docs: { path: docsByProvider.get(option.groupId)! } } : {}),
        },
        ...(option.onboardingScopes ? { onboardingScopes: [...option.onboardingScopes] } : {}),
        source: "runtime" as const,
      })),
  );
}

关键设计

  1. 条件属性展开...(option.hint ? { hint: option.hint } : {}) --- 只在属性存在时包含,保持 JSON 输出干净
  2. group 构建:从 Provider Wizard Option 的 groupId/groupLabel 构建 FlowOptionGroup
  3. docs 关联 :通过 docsByProvider.get(option.groupId) 查找文档路径
  4. onboardingScopes 复制[...option.onboardingScopes] --- 浅拷贝,避免外部修改影响 Contribution
  5. 排序 :最终调用 sortFlowContributionsByLabel 确保选项按字母顺序显示
resolveProviderModelPickerFlowContributions
typescript 复制代码
export function resolveProviderModelPickerFlowContributions(params?: { ... }): ProviderModelPickerFlowContribution[] {
  const docsByProvider = resolveProviderDocsById(params ?? {});
  return sortFlowContributionsByLabel(
    resolveProviderModelPickerEntries(params ?? {}).map((entry) => {
      // Step 1: 从 entry.value 提取 providerId
      const providerId = entry.value.startsWith("provider-plugin:")
        ? entry.value.slice("provider-plugin:".length).split(":")[0]
        : entry.value;
      return {
        id: `provider:model-picker:${entry.value}`,
        kind: "provider" as const,
        surface: "model-picker" as const,
        providerId,
        option: {
          value: entry.value,
          label: entry.label,
          ...(entry.hint ? { hint: entry.hint } : {}),
          ...(docsByProvider.get(providerId) ? { docs: { path: docsByProvider.get(providerId)! } } : {}),
        },
        source: "runtime" as const,
      };
    }),
  );
}

providerId 提取逻辑

  • 如果 value 以 "provider-plugin:" 开头(如 "provider-plugin:openai:api-key"),提取 : 之间的第一段("openai"
  • 否则 value 本身就是 providerId

2.4 Doctor Health Flow 逐行解析

doctor-health.ts(命令入口)
typescript 复制代码
const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message);
const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message);

设计 :包装 @clack/prompts 的 intro/outro,统一添加样式。stylePromptTitle 可能返回 null(如果样式化后为空),此时 fallback 到原始 message。

typescript 复制代码
export async function doctorCommand(
  runtime: RuntimeEnv = defaultRuntime,
  options: DoctorOptions = {},
) {
  const prompter = createDoctorPrompter({ runtime, options });
  printWizardHeader(runtime);
  intro("OpenClaw doctor");

逐步解析

  1. createDoctorPrompter --- 创建交互式提示器(封装 confirm/select/note)
  2. printWizardHeader --- 打印 OpenClaw 版本头部
  3. intro("OpenClaw doctor") --- 显示 @clack 的 intro 横幅
typescript 复制代码
  const root = await resolveOpenClawPackageRoot({
    moduleUrl: import.meta.url,
    argv1: process.argv[1],
    cwd: process.cwd(),
  });

设计 :解析 OpenClaw 包根目录,用于后续的源码安装检查和路径定位。使用 import.meta.url 确保在 ESM 环境下正确工作。

typescript 复制代码
  const updateResult = await maybeOfferUpdateBeforeDoctor({
    runtime, options, root,
    confirm: (p) => prompter.confirm(p),
    outro,
  });
  if (updateResult.handled) {
    return;  // 用户选择更新,退出 doctor
  }

设计 :在诊断前先检查是否有可用更新。如果用户选择更新,handled = true,doctor 终止(因为更新后配置可能改变,需重新运行)。

typescript 复制代码
  await maybeRepairUiProtocolFreshness(runtime, prompter);
  noteSourceInstallIssues(root);
  noteStartupOptimizationHints();

前置检查

  1. maybeRepairUiProtocolFreshness --- 修复 UI 协议版本过期
  2. noteSourceInstallIssues --- 报告源码安装问题
  3. noteStartupOptimizationHints --- 启动优化提示(如 macOS keychain 访问提示)
typescript 复制代码
  const configResult = await loadAndMaybeMigrateDoctorConfig({
    options,
    confirm: (p) => prompter.confirm(p),
  });
  const ctx = {
    runtime,
    options,
    prompter,
    configResult,
    cfg: configResult.cfg,
    cfgForPersistence: structuredClone(configResult.cfg),
    sourceConfigValid: configResult.sourceConfigValid ?? true,
    configPath: configResult.path ?? CONFIG_PATH,
  };
  await runDoctorHealthContributions(ctx);
  outro("Doctor complete.");
}

上下文构建

  1. loadAndMaybeMigrateDoctorConfig --- 加载配置,必要时迁移
  2. cfg: configResult.cfg --- 当前配置(会被检查修改)
  3. cfgForPersistence: structuredClone(configResult.cfg) --- 关键设计 :深拷贝初始配置作为比较基准。最终比较 cfgcfgForPersistence,判断是否需要写回
  4. sourceConfigValid --- 配置源是否有效(影响某些检查是否跳过)
doctor-health-contributions.ts(27 项检查)

这是整个 flows 目录最大的文件(637 行),定义了 27 项 Doctor 健康检查。核心架构:

typescript 复制代码
export type DoctorHealthContribution = FlowContribution & {
  kind: "core";
  surface: "health";
  run: (ctx: DoctorHealthFlowContext) => Promise<void>;  // 关键:每个检查是一个 async 函数
};

设计模式 :每个检查是一个 DoctorHealthContribution,包含:

  • 标准的 FlowContribution 字段(id, kind, surface, option)
  • 额外的 run 函数 --- 执行检查逻辑

工厂函数

typescript 复制代码
function createDoctorHealthContribution(params: {
  id: string;
  label: string;
  hint?: string;
  run: (ctx: DoctorHealthFlowContext) => Promise<void>;
}): DoctorHealthContribution {
  return {
    id: params.id,
    kind: "core",
    surface: "health",
    option: { value: params.id, label: params.label, ...(params.hint ? { hint: params.hint } : {}) },
    source: "doctor",
    run: params.run,
  };
}

27 项检查清单及依赖关系

# ID Label 修改 cfg? 依赖前序结果?
1 doctor:gateway-config Gateway config
2 doctor:auth-profiles Auth profiles ✅ (maybeRepairLegacyOAuth)
3 doctor:claude-cli Claude CLI
4 doctor:gateway-auth Gateway auth ✅ (可能设置 token) gatewayDetails (from #2)
5 doctor:legacy-state Legacy state ✅ (迁移旧状态)
6 doctor:legacy-plugin-manifests Legacy plugin manifests
7 doctor:bundled-plugin-runtime-deps Bundled plugin runtime deps
8 doctor:state-integrity State integrity
9 doctor:session-locks Session locks
10 doctor:legacy-cron Legacy cron
11 doctor:sandbox Sandbox ✅ (maybeRepairSandboxImages)
12 doctor:gateway-services Gateway services
13 doctor:startup-channel-maintenance Startup channel maintenance shouldRepair
14 doctor:security Security
15 doctor:browser Browser
16 doctor:oauth-tls OAuth TLS
17 doctor:hooks-model Hooks model
18 doctor:systemd-linger systemd linger Linux + local
19 doctor:workspace-status Workspace status
20 doctor:bootstrap-size Bootstrap size
21 doctor:shell-completion Shell completion
22 doctor:gateway-health Gateway health ✅ → healthOk, gatewayMemoryProbe
23 doctor:memory-search Memory search gatewayMemoryProbe (from #22)
24 doctor:gateway-daemon Gateway daemon healthOk (from #22)
25 doctor:write-config Write config ✅ (最终写回) cfg ≠ cfgForPersistence
26 doctor:workspace-suggestions Workspace suggestions
27 doctor:final-config-validation Final config validation

顺序至关重要

  • #2 (auth-profiles) 必须在 #4 (gateway-auth) 之前,因为 #4 依赖 ctx.gatewayDetails
  • #22 (gateway-health) 必须在 #23, #24 之前,因为它们依赖 healthOkgatewayMemoryProbe
  • #25 (write-config) 必须在所有可能修改 cfg 的检查之后

关键检查深度解析

runGatewayAuthHealth(#4)--- Gateway 认证检查:

复制代码
1. 检查 resolveDoctorMode(cfg) === "local" --- 远程模式跳过
2. 检查 sourceConfigValid --- 配置源无效则跳过
3. 解析 gateway.auth.token SecretRef
4. resolveGatewayAuth() → 确定 auth mode (token/password/off)
5. needsToken = (mode 不是 password) && (mode 不是 token 或 token 为空)
6. 如果不需要 token → 返回
7. 如果 token 是 SecretRef 管理 → 不覆盖,提示用户解决外部 Secret
8. 否则提示生成 gateway token
9. 用户确认后 → randomToken() 生成 → 写入 cfg

runGatewayHealthChecks(#22)--- Gateway 存活性检查:

复制代码
1. checkGatewayHealth({ timeoutMs }) → HTTP 探测 Gateway
   - 交互模式: 10s 超时
   - 非交互模式: 3s 超时
2. ctx.healthOk = healthOk → 传递给后续检查
3. 如果健康 → probeGatewayMemoryStatus() → 检查内存使用
4. ctx.gatewayMemoryProbe → 传递给 memory-search 检查

runWriteConfigHealth(#25)--- 配置写回:

复制代码
1. shouldWriteConfig = configResult.shouldWriteConfig || cfg ≠ cfgForPersistence
   - shouldWriteConfig: 加载时标记需要写回
   - cfg ≠ cfgForPersistence: 运行期间有检查修改了 cfg
2. 如果需要写回 → applyWizardMetadata(cfg, { command, mode }) → 写入配置文件
3. 如果不需要写回 → 提示用户运行 "openclaw doctor --fix"

2.5 Channel Setup Flow 逐行解析

三大文件职责划分

文件 职责 核心导出
channel-setup.ts 主流程编排 setupChannels()
channel-setup.status.ts 状态收集 + Contribution 构建 collectChannelStatus(), resolveChannelSetupSelectionContributions()
channel-setup.prompts.ts 专用提示逻辑 maybeConfigureDmPolicies(), promptConfiguredAction()
setupChannels() --- 主流程逐行解析
typescript 复制代码
export async function setupChannels(
  cfg: OpenClawConfig,
  runtime: RuntimeEnv,
  prompter: WizardPrompter,
  options?: SetupChannelsOptions,
): Promise<OpenClawConfig> {
  let next = cfg;  // 可变配置引用

设计 :使用 let next 而非 const cfg,因为向导的每一步都可能修改配置。

typescript 复制代码
  const scopedPluginsById = new Map<ChannelChoice, ChannelSetupPlugin>();
  const resolveWorkspaceDir = () => resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));

scopedPluginsById:按需加载的插件缓存。新发现的 Channel 插件放入此 Map,避免每次都重新查找。

resolveWorkspaceDir :惰性计算工作区目录,每次调用时基于当前 next 配置(因为配置可能在流程中改变)。

typescript 复制代码
  const getVisibleChannelPlugin = (channel: ChannelChoice): ChannelSetupPlugin | undefined =>
    scopedPluginsById.get(channel) ?? getChannelSetupPlugin(channel);

查找顺序:先查 scoped 缓存,再查全局注册表。scoped 优先(更可能是最新版本)。

typescript 复制代码
  await preloadConfiguredExternalPlugins();

预加载:加载所有已配置的外部插件(安全考虑:只加载显式启用或已配置的,不加载未启用且未配置的)。

typescript 复制代码
  const { statusByChannel, statusLines } = await collectChannelStatus({ ... });
  if (!options?.skipStatusNote && statusLines.length > 0) {
    await prompter.note(statusLines.join("\n"), "Channel status");
  }

状态收集 :调用 collectChannelStatus 收集所有渠道的配置状态,显示给用户。

typescript 复制代码
  const shouldConfigure = options?.skipConfirm
    ? true
    : await prompter.confirm({ message: "Configure chat channels now?", initialValue: true });
  if (!shouldConfigure) { return cfg; }

确认 :非快速开始模式下,询问用户是否继续。返回原始 cfg(而非 next),因为用户没有做任何修改。

typescript 复制代码
  if (options?.quickstartDefaults) {
    // QuickStart 模式:只选一次
    const choice = await prompter.select({ ... });
    if (choice !== "__skip__") {
      await handleChannelChoice(choice);
    }
  } else {
    // 正常模式:循环选择直到 Finished
    while (true) {
      const choice = await prompter.select({ ... });
      if (choice === doneValue) { break; }
      await handleChannelChoice(choice);
    }
  }

两种模式

  1. QuickStart --- 只选一个渠道,适合新用户快速上手
  2. Normal --- 循环选择,可以配置多个渠道,直到选择 "Finished"
typescript 复制代码
  next = await maybeConfigureDmPolicies({ cfg: next, selection, prompter, ... });
  return next;

DM Policy:最后一步,为所有选中的渠道配置 DM 访问策略。

handleChannelChoice() --- 单渠道处理流程
复制代码
1. 检查渠道是否在 catalog 中(需要安装插件)
   ├─ 是 → ensureChannelSetupPluginInstalled() → loadScopedChannelPlugin()
   └─ 否 → 检查是否在 installedCatalog 中
       ├─ 是 → loadScopedChannelPlugin()
       └─ 否 → enableBundledPluginForSetup()

2. 检查渠道是否已配置
   ├─ 有 configureInteractive → 使用自定义适配器流程
   ├─ 已配置 → handleConfiguredChannel()
   │   ├─ 有 configureWhenConfigured → 自定义流程
   │   └─ 否 → promptConfiguredAction() → update/disable/delete/skip
   └─ 未配置 → configureChannel()

3. applySetupResult() → 更新 cfg + 记录 accountId + 刷新状态
collectChannelStatus() --- 状态收集
typescript 复制代码
export async function collectChannelStatus(params: { ... }): Promise<ChannelStatusSummary> {
  // Step 1: 获取已安装插件列表
  const installedPlugins = params.installedPlugins ?? listChannelSetupPlugins();

  // Step 2: 解析渠道条目(已安装 + 可安装)
  const { installedCatalogEntries, installableCatalogEntries } = resolveChannelSetupEntries({ ... });

  // Step 3: 收集每个已安装插件的状态
  const statusEntries = await Promise.all(
    installedPlugins.flatMap((plugin) => {
      if (!shouldShowChannelInSetup(plugin.meta)) return [];
      const adapter = resolveAdapter(plugin.id);
      if (!adapter) return [];
      return adapter.getStatus({ cfg, options, accountOverrides });
    }),
  );

  // Step 4: 收集 fallback 状态(内置渠道但插件未加载)
  const fallbackStatuses = listChatChannels()
    .filter((meta) => shouldShowChannelInSetup(meta))
    .filter((meta) => !statusByChannel.has(meta.id))
    .map((meta) => ({ channel: meta.id, configured: isChannelConfigured(cfg, meta.id), ... }));

  // Step 5: 收集已安装目录条目状态
  const discoveredPluginStatuses = installedCatalogEntries
    .filter((entry) => !statusByChannel.has(entry.id as ChannelChoice))
    .map(...);

  // Step 6: 收集可安装目录条目状态
  const catalogStatuses = installableCatalogEntries.map(...);

  // Step 7: 合并所有状态
  const combinedStatuses = [...statusEntries, ...fallbackStatuses, ...discoveredPluginStatuses, ...catalogStatuses];
}

四层状态来源

  1. Plugin Adapter --- 已加载插件的精确状态
  2. Fallback --- 内置渠道的基础状态(仅判断 configured/unconfigured)
  3. Discovered Plugin --- 已安装但未加载的目录条目
  4. Catalog --- 可安装的目录条目

核心类型
typescript 复制代码
export type SearchProvider = NonNullable<
  NonNullable<NonNullable<NonNullable<OpenClawConfig["tools"]>["web"]>["search"]>["provider"]
>;
// 类型 = string(搜索 Provider ID)

四层 NonNullableconfig.tools?.web?.search?.provider 的可选链层层展开。最终 SearchProvider = string

Secret Input 模式
typescript 复制代码
function resolveSearchSecretInput(
  config: OpenClawConfig,
  provider: SearchProvider,
  key: string,
  secretInputMode?: SecretInputMode,
): SecretInput {
  const useSecretRefMode = secretInputMode === "ref";
  if (useSecretRefMode) {
    return buildSearchEnvRef(config, provider);  // 返回 SecretRef { source: "env", provider, id }
  }
  return key;  // 返回明文 API key
}

两种模式

  1. Plaintext(默认)--- API key 直接存入配置文件
  2. SecretRef --- 只存储对环境变量的引用({ source: "env", provider: "default", id: "BRAVE_API_KEY" }),实际值从 Gateway 环境变量读取

安全设计:SecretRef 模式避免 API key 出现在配置文件中,适合共享配置或版本控制场景。

buildSearchEnvRef
typescript 复制代码
function buildSearchEnvRef(config: OpenClawConfig, provider: SearchProvider): SecretRef {
  const entry = resolveSearchProviderEntry(config, provider) ?? ...;
  const resolvedEnvVar =
    entry?.envVars.find((k) => Boolean(normalizeOptionalString(process.env[k]))) ??
    entry?.envVars[0];
  if (!resolvedEnvVar) {
    throw new Error(`No env var mapping for search provider "${provider}"...`);
  }
  return { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: resolvedEnvVar };
}

环境变量查找策略

  1. 在 Provider 定义的 envVars 列表中,找第一个已有值的环境变量
  2. 如果没有已设置的,取第一个环境变量名作为默认
  3. 如果 Provider 没有定义任何环境变量,抛出错误
preserveDisabledState
typescript 复制代码
function preserveDisabledState(original: OpenClawConfig, result: OpenClawConfig): OpenClawConfig {
  if (original.tools?.web?.search?.enabled !== false) {
    return result;  // 原始未禁用,不需要处理
  }
  // 原始被禁用 → 确保结果也是禁用的
  const next = { ...result, tools: { ...result.tools, web: { ...result.tools?.web,
    search: { ...result.tools?.web?.search, enabled: false } } } };
  // 同时保留插件的 enabled 状态
  ...
  return next;
}

设计目的保护性拷贝 。如果用户主动禁用了搜索,设置流程不应重新启用它。这是尊重用户意图的设计原则。

runSearchSetupFlow() --- 主流程
复制代码
1. 检查可用 Provider 列表 → 空则提示启用插件
2. 显示介绍信息
3. 确定默认 Provider(已有 > env 检测 > 列表首项)
4. 用户选择 Provider(含 Skip 选项)
5. 如果 Skip → 返回原始配置
6. 解析选中的 Provider Entry
7. 检查认证状态:
   a. QuickStart + 已有 key/env → 自动应用
   b. 无需 key → 提示 key-free → 直接应用
   c. SecretRef 模式 + 已有 key → 应用选择
   d. SecretRef 模式 + 无 key → 构建环境变量引用
   e. 交互模式 → 提示输入 API key
   f. 无 key 输入但有现有 key → 保留现有
   g. 无 key 但 env 可用 → 应用选择
   h. 完全无 key → 提示获取 key,设置 provider 但不设 enabled
8. finalizeSearchProviderSetup() → 执行 Provider 的 runSetup 钩子

2.7 Model Picker Flow 逐行解析

模块职责:将模型目录(catalog)转化为可浏览的选择器,支持 Provider 过滤、认证检查、Allowlist 管理。

关键常量
typescript 复制代码
const KEEP_VALUE = "__keep__";        // 保留当前模型
const MANUAL_VALUE = "__manual__";    // 手动输入模型名
const PROVIDER_FILTER_THRESHOLD = 30; // 超过 30 个模型时提示 Provider 过滤
const HIDDEN_ROUTER_MODELS = new Set(["openrouter/auto"]);  // 隐藏的 Router 内部模型
createProviderAuthChecker
typescript 复制代码
function createProviderAuthChecker(params: {
  cfg: OpenClawConfig;
  agentDir?: string;
}): (provider: string) => boolean {
  const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
  const authCache = new Map<string, boolean>();
  return (provider: string) => {
    const cached = authCache.get(provider);
    if (cached !== undefined) return cached;
    const value = hasAuthForProvider(provider, params.cfg, authStore);
    authCache.set(provider, value);
    return value;
  };
}

设计 :闭包 + 缓存。创建一个认证检查函数,缓存结果避免重复检查。allowKeychainPrompt: false 确保在模型选择器中不弹出 Keychain 提示。

addModelSelectOption
typescript 复制代码
function addModelSelectOption(params: { ... }) {
  const key = modelKey(params.entry.provider, params.entry.id);  // "openai/gpt-4o"
  if (params.seen.has(key) || HIDDEN_ROUTER_MODELS.has(key)) return;  // 去重 + 隐藏
  const hints: string[] = [];
  if (params.entry.name && params.entry.name !== params.entry.id) hints.push(params.entry.name);
  if (params.entry.contextWindow) hints.push(`ctx ${formatTokenK(params.entry.contextWindow)}`);
  if (params.entry.reasoning) hints.push("reasoning");
  const aliases = params.aliasIndex.byKey.get(key);
  if (aliases?.length) hints.push(`alias: ${aliases.join(", ")}`);
  const routeHint = resolveModelRouteHint(params.entry.provider);
  if (routeHint) hints.push(routeHint);
  if (!params.hasAuth(params.entry.provider)) hints.push("auth missing");  // 关键:标记缺认证
  params.options.push({ value: key, label: key, hint: hints.length > 0 ? hints.join(" · ") : undefined });
  params.seen.add(key);
}

Hint 构建:将多个维度的信息合并为一个提示字符串:

  • 模型全名(如果不同于 ID)
  • 上下文窗口大小(格式化为 "128K")
  • 是否支持推理(reasoning)
  • 别名列表
  • API 路由提示("API key route" / "ChatGPT OAuth route")
  • 认证缺失("auth missing")--- 最关键的提示,告知用户此模型需要先配置认证
maybeFilterModelsByProvider
typescript 复制代码
async function maybeFilterModelsByProvider(params: { ... }): Promise<typeof params.models> {
  const providerIds = Array.from(new Set(params.models.map((entry) => entry.provider))).toSorted(...);
  const hasPreferredProvider = !!params.preferredProvider;
  const shouldPromptProvider =
    !hasPreferredProvider &&
    providerIds.length > 1 &&
    params.models.length > PROVIDER_FILTER_THRESHOLD;

  if (shouldPromptProvider) {
    // 提示用户选择 Provider 过滤
    const selection = await prompter.select({
      message: "Filter models by provider",
      options: [{ value: "*", label: "All providers" }, ...buildProviderOptions],
    });
    if (selection !== "*") next = next.filter((entry) => entry.provider === selection);
  }

  if (hasPreferredProvider) {
    // 自动过滤到首选 Provider
    const filtered = next.filter((entry) => matchesPreferredProvider?.(entry.provider));
    if (filtered.length > 0) next = filtered;
  }
  return next;
}

智能过滤

  1. 如果指定了 preferredProvider → 自动过滤
  2. 如果模型数量 > 30 且有多个 Provider → 提示用户选择 Provider
  3. 否则显示所有模型

PROVIDER_FILTER_THRESHOLD = 30:30 个模型以下可以直接选择,超过后建议先过滤 Provider 减少选项。

promptDefaultModel() --- 主流程
复制代码
1. 解析当前配置的模型 → configuredRaw, resolvedKey
2. 加载模型目录 → catalog
3. 如果目录为空 → promptManualModel() fallback
4. 构建 Alias 索引
5. 根据 Allowlist 过滤目录
6. maybeFilterModelsByProvider() → filteredModels
7. 构建选项列表:
   a. "Keep current" (allowKeep)
   b. "Enter model manually" (includeManual)
   c. Provider Plugin Setups (includeProviderPluginSetups)
   d. 所有 filteredModels 的选项
8. 确定初始值
9. 用户选择
10. 处理选择:
    - KEEP → 返回空(不改变)
    - MANUAL → promptManualModel()
    - Provider Plugin → maybeHandleProviderPluginSelection()
    - 模型 → runProviderModelSelectedHook() + 返回 { model }
promptModelAllowlist() --- Allowlist 管理
复制代码
1. 收集现有 keys + allowedKeys + initialSelections → initialKeys
2. 加载目录 → catalog
3. 如果目录为空 → 手动输入模式
4. 构建 Provider 过滤后的目录
5. 构建选项列表(含 "configured (not in catalog)" 补充项)
6. 用户多选
7. 如果清空 → 确认是否清除
8. 返回 { models: selected }
applyModelAllowlist
typescript 复制代码
export function applyModelAllowlist(cfg: OpenClawConfig, models: string[]): OpenClawConfig {
  const normalized = normalizeModelKeys(models);
  if (normalized.length === 0) {
    // 清空 allowlist = 删除 agents.defaults.models
    const { models: _ignored, ...restDefaults } = defaults;
    return { ...cfg, agents: { ...cfg.agents, defaults: restDefaults } };
  }
  // 构建新 allowlist,保留已有别名
  const nextModels: Record<string, { alias?: string }> = {};
  for (const key of normalized) {
    nextModels[key] = existingModels[key] ?? {};  // 保留已有别名,新项为空对象
  }
  return { ...cfg, agents: { ...cfg.agents, defaults: { ...defaults, models: nextModels } } };
}

设计:清空 allowlist 表示"允许所有模型"(删除 models 字段)。添加 allowlist 保留已有条目的 alias 配置。

applyModelFallbacksFromSelection
typescript 复制代码
export function applyModelFallbacksFromSelection(
  cfg: OpenClawConfig,
  selection: string[],
): OpenClawConfig {
  const normalized = normalizeModelKeys(selection);
  if (normalized.length <= 1) return cfg;  // 只有 1 个模型,不需要 fallback
  const resolvedKey = modelKey(resolved.provider, resolved.model);
  if (!normalized.includes(resolvedKey)) return cfg;  // 当前模型不在选择中,不设置 fallback
  const fallbacks = normalized.filter((key) => key !== resolvedKey);
  return { ...cfg, agents: { ...cfg.agents, defaults: {
    ...defaults, model: { ...(typeof existingModel === "object" ? existingModel : undefined),
      primary: existingPrimary ?? resolvedKey, fallbacks,
    } } } };
}

设计:从多选结果中,将除 primary 外的模型设为 fallback。这样当 primary 模型不可用时,系统自动降级到 fallback。


三、跨模块调用关系与数据流

3.1 依赖方向

复制代码
types.ts ← provider-flow.ts ← (外部消费者: model-picker.runtime, onboard-helpers)
    ↑
    ├── doctor-health.ts ← doctor-health-contributions.ts ← commands/doctor-*
    ├── channel-setup.ts ← channel-setup.status.ts ← channels/plugins/*
    │                    ← channel-setup.prompts.ts
    ├── search-setup.ts ← plugins/web-search-providers.*
    └── model-picker.ts ← agents/model-*, plugins/provider-model-*

types.ts 是基础,被所有其他文件依赖。没有循环依赖。

3.2 数据流入流出

模块 数据流入 数据流出
types.ts 无(纯定义) FlowContribution 类型 + merge/sort 工具
provider-flow.ts OpenClawConfig + 插件系统 ProviderSetupFlowContribution[] + ProviderModelPickerFlowContribution[]
doctor-health.ts RuntimeEnv + DoctorOptions 配置文件写入 + 终端输出
doctor-health-contributions.ts DoctorHealthFlowContext 修改 ctx.cfg + ctx.healthOk + ctx.gatewayMemoryProbe
channel-setup.ts OpenClawConfig + RuntimeEnv + WizardPrompter 修改后的 OpenClawConfig + PostWriteHooks
channel-setup.status.ts OpenClawConfig + 插件列表 ChannelStatusSummary + ChannelSetupSelectionContribution[]
channel-setup.prompts.ts OpenClawConfig + WizardPrompter ConfiguredChannelAction + DmPolicy 修改
search-setup.ts OpenClawConfig + RuntimeEnv + WizardPrompter 修改后的 OpenClawConfig(tools.web.search)
model-picker.ts OpenClawConfig + WizardPrompter + 模型目录 { model?, config? } + { models? }

3.3 配置修改追踪模式

所有 Flow 模块都遵循相同的配置修改模式:

typescript 复制代码
let next = cfg;                              // 1. 从输入配置开始
// ... 各种检查/向导步骤 ...
next = someModifier(next);                   // 2. 每步返回新配置
// ... 更多步骤 ...
return next;                                 // 3. 返回最终配置

不可变性设计 :每个修改函数返回新的配置对象,不修改原对象。但 let next 允许在整个流程中累积修改。

Doctor 的特殊设计

typescript 复制代码
cfgForPersistence: structuredClone(configResult.cfg),  // 深拷贝作为基准
// ... 27 项检查可能修改 ctx.cfg ...
if (JSON.stringify(ctx.cfg) !== JSON.stringify(ctx.cfgForPersistence)) {
  writeConfigFile(ctx.cfg);  // 只在确实有修改时写回
}

四、关键设计决策

4.1 FlowContribution 统一抽象

问题:Channel、Provider、Search、Doctor 都需要向设置向导贡献选项,但它们的领域逻辑完全不同。

方案 :定义 FlowContribution 统一类型,通过 kindsurface 区分维度,通过 option 统一展示格式。每个领域扩展 FlowContribution 添加自己的字段(如 Doctor 添加 run、Provider 添加 providerId)。

好处

  1. 统一的选项合并/排序算法(mergeFlowContributions / sortFlowContributionsByLabel
  2. UI 层只需处理 FlowOption,不需要知道来源
  3. 新领域只需注册 Contribution,无需修改向导代码

4.2 Doctor 的 Contribution 编排模式

问题:27 项检查之间有依赖关系(如 #22 的结果被 #23, #24 使用),如何管理顺序和数据传递?

方案

  1. 使用 DoctorHealthFlowContext 作为共享可变上下文
  2. 检查按顺序执行,前序检查将结果写入 ctx
  3. 后续检查从 ctx 读取前序结果

权衡:这比纯函数式方案(每步返回新 context)更简单直接,但引入了隐式依赖。选择此方案是因为:

  1. Doctor 检查数量固定,依赖关系稳定
  2. 顺序执行是 Doctor 的天然语义(用户期望逐步看到结果)
  3. 可变 context 避免了大量 context 传播代码

4.3 Channel Setup 的 Scoped Plugin 缓存

问题:Channel 插件可能在设置流程中被动态安装,如何避免每次都重新查找?

方案scopedPluginsById: Map<ChannelChoice, ChannelSetupPlugin> 作为会话级缓存

设计

  1. 新安装的插件放入 scoped 缓存
  2. 查找时先查 scoped,再查全局注册表
  3. listVisibleInstalledPlugins() 合并两个来源

好处:避免动态安装的插件需要重启才能使用。

问题:API key 是敏感信息,直接存入配置文件不安全。

方案:两种模式:

  1. Plaintext --- key 直接存入 tools.web.search.braveApiKey
  2. SecretRef --- 只存 { source: "env", provider: "default", id: "BRAVE_API_KEY" },运行时从环境变量读取

设计resolveSearchSecretInput() 根据 secretInputMode 参数决定返回哪种类型。buildSearchEnvRef() 自动查找最佳环境变量名。

4.5 Model Picker 的智能过滤

问题:模型目录可能有 100+ 个模型,直接展示给用户体验差。

方案:三层过滤:

  1. Allowlist 过滤 --- 如果配置了 agents.defaults.models,只显示允许的模型
  2. Provider 过滤 --- 如果指定了 preferredProvider,自动过滤;如果模型数 > 30,提示用户选择 Provider
  3. Auth 过滤 --- 在选项 hint 中标记 "auth missing",不直接过滤(用户可能想看到所有选项)

4.6 Channel Setup 的 Post-Write Hook

问题:某些渠道在配置写入后需要执行额外操作(如发送测试消息、注册 Webhook),但设置流程不应阻塞在这些操作上。

方案ChannelOnboardingPostWriteHook 延迟执行模式:

typescript 复制代码
type ChannelOnboardingPostWriteHook = {
  channel: ChannelChoice;
  accountId: string;
  run: (params: { cfg: OpenClawConfig; runtime: RuntimeEnv }) => Promise<void>;
};
  1. 收集阶段:每个渠道的 adapter 可以注册 hook
  2. 配置写入后:统一执行所有 hook
  3. 单个 hook 失败不影响其他 hook(try/catch 隔离)

五、模块间协作全景

5.1 openclaw setup 完整流程

复制代码
$ openclaw setup

1. Gateway 设置 → daemon/ (systemd/launchd/schtasks)
2. Provider 设置 → flows/provider-flow.ts → plugins/provider-wizard.ts
3. 模型选择 → flows/model-picker.ts → agents/model-catalog.ts
4. 渠道设置 → flows/channel-setup.ts → channels/plugins/*
5. 搜索设置 → flows/search-setup.ts → plugins/web-search-providers.*
6. DM Policy → flows/channel-setup.prompts.ts
7. 配置写入 → config/config.ts
8. Post-Write Hooks → channel-setup.ts::runCollectedChannelOnboardingPostWriteHooks()

5.2 openclaw doctor 完整流程

复制代码
$ openclaw doctor

1. 更新检查 → commands/doctor-update.ts
2. UI 协议修复 → commands/doctor-ui.ts
3. 源码安装检查 → commands/doctor-install.ts
4. 配置加载/迁移 → commands/doctor-config-flow.ts
5. 27 项健康检查 → flows/doctor-health-contributions.ts
6. 配置写回 → doctor:write-config
7. 最终验证 → doctor:final-config-validation

文档生成时间:2026-04-20 | 分析工具:OpenClaw Agent | 架构图:Dark Terminal 风格 (7 张 SVG) | 源码分析:2,430 行逐行解析

相关推荐
gyx_这个杀手不太冷静3 小时前
大人工智能时代下前端界面全新开发模式的思考(六)
前端·架构·ai编程
2603_954708314 小时前
交直流混合微电网架构:拓扑优化与功率交互设计
人工智能·分布式·物联网·架构·系统架构·能源
风曦Kisaki4 小时前
#企业级网络架构Day01:网络概述,网络参考模型,交换机命令行
网络·架构·智能路由器
147API4 小时前
多模型 fallback 怎么设计?一个可落地的简化架构
人工智能·架构·大模型api
哥布林学者4 小时前
深度学习进阶(十) RoI Align
机器学习·ai
ToddyBear4 小时前
深入Anthropic Claude AI的记忆模块的设计思想和架构
人工智能·架构
阿杰学AI4 小时前
AI核心知识131—大语言模型之 自主智能体(简洁且通俗易懂版)
人工智能·ai·语言模型·自然语言处理·agent·智能体·自主智能体
若兰幽竹4 小时前
【从零开始编写数据库系统:架构设计与实现】第1章 ToyDB全景架构与核心概念
数据库·架构·数据库内核
AIwenIPgeolocation4 小时前
豫见OpenClaw·人工智能技术交流沙龙成功举办 埃文科技受邀主讲共探数智新路径
ai