Skip to content

Commit e74c009

Browse files
feat: 添加 GrowthBook 自定义服务器适配器
通过 CLAUDE_GB_ADAPTER_URL/KEY 环境变量连接自定义 GrowthBook 实例, 无配置时所有 feature 读取返回代码默认值。支持 GrowthBook Cloud(非 remoteEval), 含完整文档和 feature key 列表。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 78144b4 commit e74c009

5 files changed

Lines changed: 213 additions & 21 deletions

File tree

DEV-LOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# DEV-LOG
22

3+
## GrowthBook 自定义服务器适配器 (2026-04-03)
4+
5+
GrowthBook 功能开关系统原为 Anthropic 内部构建设计,硬编码 SDK key 和 API 地址,外部构建因 `is1PEventLoggingEnabled()` 门控始终禁用。新增适配器模式,通过环境变量连接自定义 GrowthBook 服务器,无配置时所有 feature 读取返回代码默认值。
6+
7+
**修改文件:**
8+
9+
| 文件 | 变更 |
10+
|------|------|
11+
| `src/constants/keys.ts` | `getGrowthBookClientKey()` 优先读取 `CLAUDE_GB_ADAPTER_KEY` 环境变量 |
12+
| `src/services/analytics/growthbook.ts` | `isGrowthBookEnabled()` 适配器模式下直接返回 `true`,绕过 1P event logging 门控 |
13+
| `src/services/analytics/growthbook.ts` | `getGrowthBookClient()` base URL 优先使用 `CLAUDE_GB_ADAPTER_URL` |
14+
| `docs/internals/growthbook-adapter.mdx` | 新增适配器配置文档,含全部 ~58 个 feature key 列表 |
15+
16+
**用法:** `CLAUDE_GB_ADAPTER_URL=https://gb.example.com/ CLAUDE_GB_ADAPTER_KEY=sdk-xxx bun run dev`
17+
18+
---
19+
320
## Datadog 日志端点可配置化 (2026-04-03)
421

522
将 Datadog 硬编码的 Anthropic 内部端点改为环境变量驱动,默认禁用。

README.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@
1111
- [ ] Biome 格式化可能不会先实施, 避免代码冲突
1212
- [x] 构建流水线完成, 产物 Node/Bun 都可以运行
1313
- [x] V3 会写大量文档, 完善文档站点
14-
- [ ] V4 会完成大量的测试文件, 以提高稳定性
14+
- [x] V4 会完成大量的测试文件, 以提高稳定性
1515
- [x] Buddy 小宠物回来啦
1616
- [x] Auto Mode 回归
1717
- [x] 所有 Feature 现在可以通过环境变量配置, 而不是垃圾的 bun --feature
18+
- [x] V5 支持企业级的监控上报功能, 补全缺失的工具, 解除限制
1819
- [x] 移除牢 A 的反蒸馏代码!!!
1920
- [x] 补全 web search 能力(用的 Bing 搜索)!!!
2021
- [x] 支持 Debug
2122
- [x] 关闭自动更新;
22-
- [x] 添加 sentry 错误上报支持
23-
- [ ] V5 大规模重构石山代码, 全面模块分包
24-
- [ ] V5 将会为全新分支, 届时 main 分支将会封存为历史版本
23+
- [x] 添加自定义 sentry 错误上报支持
24+
- [x] 添加自定义 GrowthBook 支持 (GB 也是开源的, 现在你可以配置一个自定义的遥控平台)
25+
- [ ] V6 大规模重构石山代码, 全面模块分包
26+
- [ ] V6 将会为全新分支, 届时 main 分支将会封存为历史版本
2527

2628
> 我不知道这个项目还会存在多久, Star + Fork + git clone + .zip 包最稳健; 说白了就是扛旗项目, 看看能走多远
2729
>
@@ -30,14 +32,6 @@
3032
> Claude 已经烧了 1000$ 以上, 没钱了, 换成 GLM 继续玩; @zai-org GLM 5.1 非常可以;
3133
>
3234
33-
存活记录:
34-
35-
1. 开源后 36 小时, 9.7k star; 特权模式可以开启了, 新 feature 调控也加上了;加把劲明天就 10k 了;
36-
2. 开源后 48 小时: 突破 7k Star; 测试代码小有成效;
37-
3. 开源后 24 小时: 突破 6k Star, 感谢各位支持. 完成 docs 文档的站点构建, 达到 v3 版本, 后续开始进行测试用例维护, 完成之后可以接受 PR; 看来牢 A 是不想理我们了;
38-
4. 开源后 15 小时: 完成了构建产物的 node 支持, 现在是完全体了; star 快到 3k 了; 等待牢 A 的邮件
39-
5. 开源后 12 小时: 愚人节, star 破 1k, 并且牢 A 没有发邮件搞这个项目
40-
4135
## 快速开始
4236

4337
### 环境要求
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
title: "GrowthBook 适配器 - 自定义 Feature Flag 服务器接入"
3+
description: "通过环境变量连接自定义 GrowthBook 服务器,实现远程 feature flag 控制。无配置时自动回退到代码默认值。"
4+
keywords: ["growthbook", "feature flags", "远程配置", "适配器", "环境变量"]
5+
---
6+
7+
## 概述
8+
9+
Claude Code 的 GrowthBook 系统支持通过环境变量连接自定义 GrowthBook 服务器,实现远程 feature flag 控制。
10+
11+
- **有配置时**:连接你的 GrowthBook 实例,拉取并缓存 feature 值
12+
- **无配置时**:所有 feature 读取直接返回代码中的默认值,零网络请求
13+
14+
## 环境变量
15+
16+
| 变量 | 必填 | 说明 |
17+
|---|---|---|
18+
| `CLAUDE_GB_ADAPTER_URL` || GrowthBook API 地址,如 `https://gb.example.com/` |
19+
| `CLAUDE_GB_ADAPTER_KEY` || GrowthBook SDK Client Key,如 `sdk-xxxxx` |
20+
21+
两个变量都设置时启用适配器模式,否则完全跳过 GrowthBook。
22+
23+
## 使用方式
24+
25+
### 基本用法
26+
27+
```bash
28+
CLAUDE_GB_ADAPTER_URL=https://gb.example.com/ \
29+
CLAUDE_GB_ADAPTER_KEY=sdk-abc123 \
30+
bun run dev
31+
```
32+
33+
### 不使用 GrowthBook(默认行为)
34+
35+
```bash
36+
bun run dev
37+
# 所有 getFeatureValue_CACHED_MAY_BE_STALE("xxx", defaultValue) 直接返回 defaultValue
38+
```
39+
40+
## GrowthBook 服务端配置
41+
42+
### 步骤
43+
44+
1. **部署 GrowthBook 服务端**(Docker 自托管或 Cloud 版)
45+
2. **创建 Environment**(如 `production`
46+
3. **创建 SDK Connection**,获得 SDK Key(即 `CLAUDE_GB_ADAPTER_KEY`
47+
4. **按需添加 Feature**,key 和类型见下方列表
48+
49+
### 核心原则
50+
51+
- **不配置任何 feature 也能正常运行**——代码中每个调用都提供了默认值
52+
- 只创建你想远程控制的 feature,其余走代码默认
53+
- GrowthBook 上配了某个 feature 后,其值会覆盖代码中的默认值
54+
55+
## Feature Key 列表
56+
57+
### 高频使用
58+
59+
| Feature Key | 类型 | 代码默认值 | 用途 |
60+
|---|---|---|---|
61+
| `tengu_hive_evidence` | boolean | `false` | 任务证据系统 |
62+
| `tengu_quartz_lantern` | boolean | `false` | 文件写入/编辑保护 |
63+
| `tengu_auto_background_agents` | boolean | `false` | 自动后台 Agent |
64+
| `tengu_agent_list_attach` | boolean | `false` | Agent 列表附件 |
65+
| `tengu_amber_stoat` | boolean | `true` | 内置 Agents |
66+
| `tengu_slim_subagent_claudemd` | boolean | `true` | 子 Agent CLAUDE.md |
67+
| `tengu_attribution_header` | boolean | `true` | API 归因 Header |
68+
| `tengu_cobalt_harbor` | boolean | `false` | Bridge 模式 |
69+
| `tengu_ccr_bridge` | boolean | `false` | CCR Bridge |
70+
| `tengu_cicada_nap_ms` | number | `0` | 后台刷新节流(毫秒) |
71+
| `tengu_miraculo_the_bard` | boolean | `false` | 启动欢迎信息 |
72+
73+
### Agent / 工具控制
74+
75+
| Feature Key | 类型 | 代码默认值 | 用途 |
76+
|---|---|---|---|
77+
| `tengu_surreal_dali` | boolean | `false` | 远程触发工具 |
78+
| `tengu_glacier_2xr` | boolean | `false` | 工具搜索增强 |
79+
| `tengu_plum_vx3` | boolean | `false` | Web Search 使用 Haiku |
80+
| `tengu_destructive_command_warning` | boolean | `false` | 危险命令警告 |
81+
| `tengu_birch_trellis` | boolean | `true` | Bash 权限控制 |
82+
| `tengu_harbor_permissions` | boolean | `false` | Harbor 权限模式 |
83+
84+
### Bridge / 远程连接
85+
86+
| Feature Key | 类型 | 代码默认值 | 用途 |
87+
|---|---|---|---|
88+
| `tengu_bridge_repl_v2` | boolean | `false` | Bridge REPL v2 |
89+
| `tengu_copper_bridge` | boolean | `false` | Copper Bridge |
90+
| `tengu_ccr_mirror` | boolean | `false` | CCR Mirror |
91+
92+
### 内存 / 上下文
93+
94+
| Feature Key | 类型 | 代码默认值 | 用途 |
95+
|---|---|---|---|
96+
| `tengu_coral_fern` | boolean | `false` | 内存目录功能 |
97+
| `tengu_passport_quail` | boolean | `false` | 内存路径配置 |
98+
| `tengu_slate_thimble` | boolean | `false` | Slate Thimble |
99+
| `tengu_herring_clock` | boolean | `false` | 跳过索引 |
100+
| `tengu_session_memory` | boolean | `false` | 会话内存 |
101+
| `tengu_pebble_leaf_prune` | boolean | `false` | 内存修剪 |
102+
103+
### UI / 体验
104+
105+
| Feature Key | 类型 | 代码默认值 | 用途 |
106+
|---|---|---|---|
107+
| `tengu_terminal_sidebar` | boolean | `false` | 终端侧边栏 |
108+
| `tengu_terminal_panel` | boolean | `false` | 终端面板 |
109+
| `tengu_willow_mode` | boolean | `false` | Willow 模式 |
110+
| `tengu_collage_kaleidoscope` | boolean | `false` | UI 效果 |
111+
| `tengu_chrome_auto_enable` | boolean | `false` | Chrome 自动启用 |
112+
| `tengu_immediate_model_command` | boolean | `false` | 即时模型切换 |
113+
| `tengu_remote_backend` | boolean | `false` | 远程后端 |
114+
115+
### 配置对象(动态配置)
116+
117+
| Feature Key | 类型 | 代码默认值 | 用途 |
118+
|---|---|---|---|
119+
| `tengu_file_read_limits` | object | `null` | 文件读取限制配置 |
120+
| `tengu_cobalt_raccoon` | object | `null` | Cobalt 配置 |
121+
| `tengu_cobalt_lantern` | object | `null` | Lantern 配置 |
122+
| `tengu_desktop_upsell` | object | `null` | 桌面版引导 |
123+
| `tengu_marble_sandcastle` | object | `null` | Marble 配置 |
124+
| `tengu_marble_fox` | object | `null` | Marble Fox 配置 |
125+
| `tengu_ultraplan_model` | string | `null` | Ultraplan 模型名 |
126+
127+
### Gate(布尔门控)
128+
129+
| Gate Key | 代码默认值 | 用途 |
130+
|---|---|---|
131+
| `tengu_chair_sermon` | `false` | 功能门控 |
132+
| `tengu_scratch` | `false` | Scratch 功能 |
133+
| `tengu_thinkback` | `false` | Thinkback 功能 |
134+
| `tengu_tool_pear` | `false` | Tool Pear 功能 |
135+
136+
## 读取优先级链
137+
138+
每个 feature 的值按以下顺序解析,第一个命中即返回:
139+
140+
```
141+
1. CLAUDE_INTERNAL_FC_OVERRIDES 环境变量(JSON 对象覆盖)
142+
↓ 未命中
143+
2. growthBookOverrides 配置(~/.claude.json,仅 ant 构建)
144+
↓ 未命中
145+
3. 内存缓存(remoteEvalFeatureValues,本次进程从服务器拉取)
146+
↓ 未命中
147+
4. 磁盘缓存(~/.claude.json 的 cachedGrowthBookFeatures)
148+
↓ 未命中
149+
5. 代码中的 defaultValue 参数
150+
```
151+
152+
## 缓存与刷新机制
153+
154+
| 机制 | 说明 |
155+
|---|---|
156+
| **磁盘缓存** | `~/.claude.json``cachedGrowthBookFeatures` 字段,跨进程持久化 |
157+
| **周期刷新** | 每 6 小时自动从服务器拉取最新值(`setInterval` + `unref`|
158+
| **初始化超时** | 首次连接超时 5 秒,超时后使用磁盘缓存或默认值 |
159+
| **Auth 变更** | 登录/登出时自动销毁并重建客户端 |
160+
161+
## 实现细节
162+
163+
修改了 2 个文件共 3 处:
164+
165+
1. **`src/constants/keys.ts`**`getGrowthBookClientKey()` 优先读取 `CLAUDE_GB_ADAPTER_KEY`
166+
2. **`src/services/analytics/growthbook.ts`**`isGrowthBookEnabled()` 适配器模式下直接启用
167+
3. **`src/services/analytics/growthbook.ts`** — base URL 优先使用 `CLAUDE_GB_ADAPTER_URL`
168+
169+
所有 130+ 个调用方文件无需修改。

src/constants/keys.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { isEnvTruthy } from '../utils/envUtils.js'
33
// Lazy read so ENABLE_GROWTHBOOK_DEV from globalSettings.env (applied after
44
// module load) is picked up. USER_TYPE is a build-time define so it's safe.
55
export function getGrowthBookClientKey(): string {
6+
// 适配器优先:自定义 GrowthBook 服务器
7+
const adapterKey = process.env.CLAUDE_GB_ADAPTER_KEY
8+
if (adapterKey) return adapterKey
9+
610
return process.env.USER_TYPE === 'ant'
711
? isEnvTruthy(process.env.ENABLE_GROWTHBOOK_DEV)
812
? 'sdk-yZQvlplybuXjYh6L'

src/services/analytics/growthbook.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,10 @@ function syncRemoteEvalToDisk(): void {
420420
* Check if GrowthBook operations should be enabled
421421
*/
422422
function isGrowthBookEnabled(): boolean {
423+
// 适配器模式:有自定义服务器配置时直接启用
424+
if (process.env.CLAUDE_GB_ADAPTER_URL && process.env.CLAUDE_GB_ADAPTER_KEY) {
425+
return true
426+
}
423427
// GrowthBook depends on 1P event logging.
424428
return is1PEventLoggingEnabled()
425429
}
@@ -495,15 +499,17 @@ const getGrowthBookClient = memoize(
495499

496500
const attributes = getUserAttributes()
497501
const clientKey = getGrowthBookClientKey()
502+
const baseUrl =
503+
process.env.CLAUDE_GB_ADAPTER_URL
504+
|| (process.env.USER_TYPE === 'ant'
505+
? process.env.CLAUDE_CODE_GB_BASE_URL || 'https://api.anthropic.com/'
506+
: 'https://api.anthropic.com/')
507+
const isAdapterMode = !!(process.env.CLAUDE_GB_ADAPTER_URL && process.env.CLAUDE_GB_ADAPTER_KEY)
498508
if (process.env.USER_TYPE === 'ant') {
499509
logForDebugging(
500510
`GrowthBook: Creating client with clientKey=${clientKey}, attributes: ${jsonStringify(attributes)}`,
501511
)
502512
}
503-
const baseUrl =
504-
process.env.USER_TYPE === 'ant'
505-
? process.env.CLAUDE_CODE_GB_BASE_URL || 'https://api.anthropic.com/'
506-
: 'https://api.anthropic.com/'
507513

508514
// Skip auth if trust hasn't been established yet
509515
// This prevents executing apiKeyHelper commands before the trust dialog
@@ -518,7 +524,8 @@ const getGrowthBookClient = memoize(
518524
const authHeaders = hasTrust
519525
? getAuthHeaders()
520526
: { headers: {}, error: 'trust not established' }
521-
const hasAuth = !authHeaders.error
527+
// 适配器模式下不需要 auth,GrowthBook Cloud 用 clientKey 即可
528+
const hasAuth = isAdapterMode || !authHeaders.error
522529
clientCreatedWithAuth = hasAuth
523530

524531
// Capture in local variable so the init callback operates on THIS client,
@@ -527,9 +534,10 @@ const getGrowthBookClient = memoize(
527534
apiHost: baseUrl,
528535
clientKey,
529536
attributes,
530-
remoteEval: true,
531-
// Re-fetch when user ID or org changes (org change = login to different org)
532-
cacheKeyAttributes: ['id', 'organizationUUID'],
537+
// remoteEval only works with Anthropic internal API, GrowthBook Cloud doesn't support it
538+
remoteEval: !isAdapterMode,
539+
// cacheKeyAttributes only valid with remoteEval
540+
...(!isAdapterMode ? { cacheKeyAttributes: ['id', 'organizationUUID'] } : {}),
533541
// Add auth headers if available
534542
...(authHeaders.error
535543
? {}
@@ -566,7 +574,7 @@ const getGrowthBookClient = memoize(
566574

567575
if (process.env.USER_TYPE === 'ant') {
568576
logForDebugging(
569-
`GrowthBook initialized successfully, source: ${result.source}, success: ${result.success}`,
577+
`GrowthBook initialized, source: ${result.source}, success: ${result.success}`,
570578
)
571579
}
572580

0 commit comments

Comments
 (0)