OpenClaw Flows 模块超深度架构分析
分析版本:2026-04-20 | 代码目录:
src/flows/| 风格:Dark Terminal | 源码行数:2,430 行(9 文件,不含测试)
一、模块定位
1.1 业务职责
src/flows/ 是 OpenClaw 的交互式配置向导引擎(Interactive Setup Wizard Engine)。它将用户引导式设置流程抽象为"Flow Contribution"体系,统一管理渠道设置、Provider 配置、搜索配置、模型选择和健康诊断五大核心场景。业务职责精确定义为:
- Flow 抽象层 (
types.ts)--- 定义FlowContribution统一贡献模型,实现跨模块可插拔的选项注册与合并 - Provider 设置流 (
provider-flow.ts)--- Provider 向导选项与 Model Picker 条目的 Flow Contribution 构建 - Doctor 健康流 (
doctor-health.ts+doctor-health-contributions.ts)--- 27 项健康检查的编排引擎,实现openclaw doctor命令 - 渠道设置流 (
channel-setup.ts+channel-setup.status.ts+channel-setup.prompts.ts)--- 多渠道(Discord/Telegram/WhatsApp 等)交互式配置向导 - 搜索设置流 (
search-setup.ts)--- Web Search Provider 选择、API Key 管理、Secret Input 模式 - 模型选择流 (
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 逐行解析)
FlowDocsLink
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)!]),
);
}
逐行解析:
resolvePluginProviders({ mode: "setup" })--- 以 setup 模式加载所有插件 Provider.filter()--- 类型守卫过滤出有 docsPath 的 Provider,同时将docsPath从string | undefined收窄为string.map()--- 将每个 Provider 映射为[id, docsPath]元组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,
})),
);
}
关键设计:
- 条件属性展开 :
...(option.hint ? { hint: option.hint } : {})--- 只在属性存在时包含,保持 JSON 输出干净 - group 构建:从 Provider Wizard Option 的 groupId/groupLabel 构建 FlowOptionGroup
- docs 关联 :通过
docsByProvider.get(option.groupId)查找文档路径 - onboardingScopes 复制 :
[...option.onboardingScopes]--- 浅拷贝,避免外部修改影响 Contribution - 排序 :最终调用
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");
逐步解析:
createDoctorPrompter--- 创建交互式提示器(封装 confirm/select/note)printWizardHeader--- 打印 OpenClaw 版本头部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();
前置检查:
maybeRepairUiProtocolFreshness--- 修复 UI 协议版本过期noteSourceInstallIssues--- 报告源码安装问题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.");
}
上下文构建:
loadAndMaybeMigrateDoctorConfig--- 加载配置,必要时迁移cfg: configResult.cfg--- 当前配置(会被检查修改)cfgForPersistence: structuredClone(configResult.cfg)--- 关键设计 :深拷贝初始配置作为比较基准。最终比较cfg和cfgForPersistence,判断是否需要写回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 之前,因为它们依赖
healthOk和gatewayMemoryProbe - #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);
}
}
两种模式:
- QuickStart --- 只选一个渠道,适合新用户快速上手
- 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];
}
四层状态来源:
- Plugin Adapter --- 已加载插件的精确状态
- Fallback --- 内置渠道的基础状态(仅判断 configured/unconfigured)
- Discovered Plugin --- 已安装但未加载的目录条目
- Catalog --- 可安装的目录条目
2.6 Search Setup Flow 逐行解析

核心类型
typescript
export type SearchProvider = NonNullable<
NonNullable<NonNullable<NonNullable<OpenClawConfig["tools"]>["web"]>["search"]>["provider"]
>;
// 类型 = string(搜索 Provider ID)
四层 NonNullable :config.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
}
两种模式:
- Plaintext(默认)--- API key 直接存入配置文件
- 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 };
}
环境变量查找策略:
- 在 Provider 定义的
envVars列表中,找第一个已有值的环境变量 - 如果没有已设置的,取第一个环境变量名作为默认
- 如果 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;
}
智能过滤:
- 如果指定了 preferredProvider → 自动过滤
- 如果模型数量 > 30 且有多个 Provider → 提示用户选择 Provider
- 否则显示所有模型
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 统一类型,通过 kind 和 surface 区分维度,通过 option 统一展示格式。每个领域扩展 FlowContribution 添加自己的字段(如 Doctor 添加 run、Provider 添加 providerId)。
好处:
- 统一的选项合并/排序算法(
mergeFlowContributions/sortFlowContributionsByLabel) - UI 层只需处理
FlowOption,不需要知道来源 - 新领域只需注册 Contribution,无需修改向导代码
4.2 Doctor 的 Contribution 编排模式
问题:27 项检查之间有依赖关系(如 #22 的结果被 #23, #24 使用),如何管理顺序和数据传递?
方案:
- 使用
DoctorHealthFlowContext作为共享可变上下文 - 检查按顺序执行,前序检查将结果写入 ctx
- 后续检查从 ctx 读取前序结果
权衡:这比纯函数式方案(每步返回新 context)更简单直接,但引入了隐式依赖。选择此方案是因为:
- Doctor 检查数量固定,依赖关系稳定
- 顺序执行是 Doctor 的天然语义(用户期望逐步看到结果)
- 可变 context 避免了大量 context 传播代码
4.3 Channel Setup 的 Scoped Plugin 缓存
问题:Channel 插件可能在设置流程中被动态安装,如何避免每次都重新查找?
方案 :scopedPluginsById: Map<ChannelChoice, ChannelSetupPlugin> 作为会话级缓存。
设计:
- 新安装的插件放入 scoped 缓存
- 查找时先查 scoped,再查全局注册表
listVisibleInstalledPlugins()合并两个来源
好处:避免动态安装的插件需要重启才能使用。
4.4 Search Setup 的 Secret Input 模式
问题:API key 是敏感信息,直接存入配置文件不安全。
方案:两种模式:
- Plaintext --- key 直接存入
tools.web.search.braveApiKey - SecretRef --- 只存
{ source: "env", provider: "default", id: "BRAVE_API_KEY" },运行时从环境变量读取
设计 :resolveSearchSecretInput() 根据 secretInputMode 参数决定返回哪种类型。buildSearchEnvRef() 自动查找最佳环境变量名。
4.5 Model Picker 的智能过滤
问题:模型目录可能有 100+ 个模型,直接展示给用户体验差。
方案:三层过滤:
- Allowlist 过滤 --- 如果配置了
agents.defaults.models,只显示允许的模型 - Provider 过滤 --- 如果指定了
preferredProvider,自动过滤;如果模型数 > 30,提示用户选择 Provider - 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>;
};
- 收集阶段:每个渠道的 adapter 可以注册 hook
- 配置写入后:统一执行所有 hook
- 单个 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 行逐行解析