gamealgo-cli 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -113
- package/cli/README.md +53 -22
- package/dist/gamealgo.js +83 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,144 +1,72 @@
|
|
|
1
1
|
# GameAlgo Client
|
|
2
2
|
|
|
3
|
-
GameAlgo Client
|
|
3
|
+
GameAlgo Client 是公开客户端仓库,包含 iOS SDK、Android SDK、REST helper、TapTap Maker / Lua SDK、CLI 和协议定义。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
产品能力、接入流程、埋点、实验、报表与优化方法由 GameAlgo Server 在线文档统一维护,不在本仓库保留副本。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## AI Agent 从这里开始
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
不要把 README 原样转发给开发者。先安装最新 CLI,再从目标环境读取当前文档并完成接入。
|
|
10
10
|
|
|
11
|
-
|
|
|
11
|
+
| 环境 | Admin host | SDK / REST API |
|
|
12
12
|
| --- | --- | --- |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
16
|
-
如果当前仓库没有真实 `ga_live_*`,不要留下 `ga_live_xxx`、`TODO_GAME_KEY`、`no-op key` 之类占位后声称完成。你必须把获取真实 Client Game Key 当作阻塞步骤。
|
|
17
|
-
|
|
18
|
-
开发者通常只需要提供游戏维度的 Game Admin Key,格式为 `ga_admin_*`。拿到 Admin Key 后,你必须用 GameAlgo CLI 自动创建或读取 Client Game Key,并把真实 `ga_live_*` 写入合适的位置。
|
|
19
|
-
|
|
20
|
-
先向开发者询问这几项:
|
|
21
|
-
|
|
22
|
-
- 使用国内环境还是海外环境
|
|
23
|
-
- Game Admin Key,格式 `ga_admin_*`。如果用户已经提供真实 `ga_live_*`,可以跳过创建 key,但仍要验证
|
|
24
|
-
- 游戏运行环境:iOS / Android / Web / Backend / TapTap Maker / TapTap 小游戏
|
|
25
|
-
- 本次目标:只接基础事件,还是同时接实验、配置、报表和自动化实验
|
|
26
|
-
|
|
27
|
-
然后按下面流程执行:
|
|
28
|
-
|
|
29
|
-
| 环境 | Admin host |
|
|
30
|
-
| --- | --- |
|
|
31
|
-
| 国内 | `https://game-algo-admin.dictapis.cn` |
|
|
32
|
-
| 海外 | `https://dirichlet.ai/algo_admin` |
|
|
13
|
+
| 国内 | `https://game-algo-admin.dictapis.cn` | `https://game-algo-sdk.dictapis.cn` |
|
|
14
|
+
| 海外 | `https://dirichlet.ai/algo_admin` | `https://dirichlet.ai/algo_sdk` |
|
|
33
15
|
|
|
34
16
|
```bash
|
|
35
17
|
npm install -g gamealgo-cli@latest
|
|
18
|
+
gamealgo --version
|
|
36
19
|
gamealgo docs agent-onboarding --host <admin-host>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
开发者通常只需要提供游戏维度的 Game Admin Key,格式为 `ga_admin_*`。在游戏项目根目录登录后,AI Agent 应通过 CLI 创建或读取运行时使用的 `ga_live_*`,完成 SDK 接入、事件验证、Report Pack 和实验配置。
|
|
23
|
+
|
|
24
|
+
```bash
|
|
37
25
|
gamealgo login --host <admin-host> --admin-key <ga_admin_xxx>
|
|
38
26
|
gamealgo key list --json
|
|
39
27
|
gamealgo key create --name <runtime-name> --json
|
|
40
28
|
gamealgo key reveal --name <runtime-name> --json
|
|
41
29
|
```
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
如果已有可用 Client Game Key,就复用或 reveal;如果没有,就创建。Client Game Key 格式为 `ga_live_*`,用于游戏运行时拉配置、拉配置文件、上报事件。
|
|
46
|
-
|
|
47
|
-
如果用户暂时不能提供 `ga_admin_*`,你可以继续完成 Scaffolded 状态的代码接入,但最终回复必须明确写出:
|
|
48
|
-
|
|
49
|
-
- 当前未激活,因为缺少真实 `ga_live_*`
|
|
50
|
-
- 不能验证配置拉取和事件上报
|
|
51
|
-
- 下一步需要用户提供 `ga_admin_*` 或真实 `ga_live_*`
|
|
52
|
-
|
|
53
|
-
继续阅读并执行:
|
|
54
|
-
|
|
55
|
-
1. [AI 接入流程](./docs/agent-onboarding.md)
|
|
56
|
-
2. [客户端接入指南](./docs/integration-guide.md)
|
|
57
|
-
3. [实验接入版本与最低版本](./docs/experiment-integration-versions.md)
|
|
58
|
-
4. [AI Agent 埋点接入手册](./docs/ai-tracking-manual.md)
|
|
59
|
-
5. [不同类型游戏埋点建议](./docs/tracking-recommendations.md)
|
|
60
|
-
6. [Report Pack 配置](./docs/report-packs.md)
|
|
61
|
-
7. [Agent CLI 命令参考](./docs/agent-cli.md)
|
|
62
|
-
8. [AI LTV 优化 Playbook](./docs/ai-ltv-optimization-playbook.md)
|
|
63
|
-
|
|
64
|
-
### 不要这样做
|
|
65
|
-
|
|
66
|
-
- 不要向开发者索要 `ga_live_*`,除非开发者明确说要手工维护运行时 key。
|
|
67
|
-
- 不要把 `ga_live_xxx` 示例值、占位 key 或 no-op key 提交到游戏工程后声称接入完成。
|
|
68
|
-
- 不要把 `ga_admin_*` 写进游戏客户端、小游戏客户端脚本、公开配置或日志。
|
|
69
|
-
- 不要把 README 原样甩给开发者后停止接入。
|
|
70
|
-
- 不要让 GameAlgo 网络请求阻塞游戏主流程。
|
|
71
|
-
- TapTap Maker / TapTap 小游戏使用 Lua SDK 客户端直连,必须配置真实 `ga_live_*`,但不能写入 `ga_admin_*`。
|
|
72
|
-
- 不要默认使用 `USD`;国内游戏、TapTap Maker / TapTap 小游戏的广告和付费事件默认使用 `CNY`。
|
|
73
|
-
|
|
74
|
-
## 如果你是开发者
|
|
31
|
+
凭据保存在当前游戏项目的 `.gamealgo/cli.json`,不同游戏互不覆盖。需要其他主题时使用:
|
|
75
32
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
开发者详细说明见 [开发者快速开始](./docs/developer-quickstart.md)。
|
|
33
|
+
```bash
|
|
34
|
+
gamealgo docs --host <admin-host>
|
|
35
|
+
gamealgo docs integration-guide --host <admin-host>
|
|
36
|
+
gamealgo docs ai-tracking-manual --host <admin-host>
|
|
37
|
+
gamealgo docs report-packs --host <admin-host>
|
|
38
|
+
gamealgo docs experiment-integration-versions --host <admin-host>
|
|
39
|
+
gamealgo docs agent-cli --host <admin-host>
|
|
40
|
+
```
|
|
85
41
|
|
|
86
|
-
##
|
|
42
|
+
## SDK 入口
|
|
87
43
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
44
|
+
- [iOS SDK](./ios/README.md)
|
|
45
|
+
- [Android SDK](./android/README.md)
|
|
46
|
+
- [REST helper](./rest-api/README.md)
|
|
47
|
+
- [TapTap Maker / Lua SDK](./lua/README.md)
|
|
48
|
+
- [CLI](./cli/README.md)
|
|
49
|
+
- [Protocol OpenAPI](./protocol/openapi.yaml)
|
|
92
50
|
|
|
93
|
-
|
|
51
|
+
海外 SDK 地址包含 `/algo_sdk` 路径前缀,必须完整保留。客户端只能配置 `ga_live_*`;`ga_admin_*` 只允许用于开发机器、AI Agent 或 CI。
|
|
94
52
|
|
|
95
|
-
##
|
|
53
|
+
## 仓库结构
|
|
96
54
|
|
|
97
55
|
```text
|
|
98
56
|
ios/ iOS Swift Package SDK
|
|
99
57
|
android/ Android Java SDK core
|
|
100
58
|
rest-api/ REST API helper 和示例
|
|
101
|
-
lua/ TapTap Maker /
|
|
102
|
-
cli/
|
|
103
|
-
|
|
104
|
-
|
|
59
|
+
lua/ TapTap Maker / Lua SDK
|
|
60
|
+
cli/ GameAlgo CLI
|
|
61
|
+
protocol/ 客户端协议定义
|
|
62
|
+
examples/ 接入示例
|
|
105
63
|
```
|
|
106
64
|
|
|
107
|
-
##
|
|
108
|
-
|
|
109
|
-
环境地址:
|
|
110
|
-
|
|
111
|
-
| 环境 | SDK / REST API | Admin / CLI |
|
|
112
|
-
| --- | --- | --- |
|
|
113
|
-
| 国内 | `https://game-algo-sdk.dictapis.cn` | `https://game-algo-admin.dictapis.cn` |
|
|
114
|
-
| 海外 | `https://dirichlet.ai/algo_sdk` | `https://dirichlet.ai/algo_admin` |
|
|
115
|
-
|
|
116
|
-
客户端 SDK 只配置 SDK / REST API 地址。Admin / CLI 地址只给开发者、AI Agent 和 CI 使用。
|
|
117
|
-
|
|
118
|
-
## SDK 接入入口
|
|
65
|
+
## 本地验证
|
|
119
66
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
所有运行时请求都通过 `X-GameAlgo-Key: ga_live_xxx` 识别游戏,不需要游戏自己传可信 `gameId`。
|
|
126
|
-
|
|
127
|
-
核心原则:SDK 后台拉取配置并写入本地快照;配置未就绪、网络失败或脚本执行失败时,游戏必须走本地默认逻辑。
|
|
128
|
-
|
|
129
|
-
## 接入后的验收
|
|
130
|
-
|
|
131
|
-
AI Agent 接入后应该至少完成这些验证:
|
|
132
|
-
|
|
133
|
-
- 客户端配置里使用真实 `ga_live_*`,不是占位值。
|
|
134
|
-
- `ga_admin_*` 没有出现在客户端代码、Info.plist、资源文件、脚本包、日志或 git diff 中。
|
|
135
|
-
- `/v1/config` 能成功返回当前游戏配置,或 SDK 日志显示 config fetched / config ready。
|
|
136
|
-
- `session_end` 和核心玩法事件能上传成功。
|
|
137
|
-
- 如果游戏使用 Adjust 等归因 SDK,归因 callback 后能回传归因;如果需要投放 ROI 报表,Adjust API Token / App Token 已在 CLI/Admin 配置并完成同步验证。
|
|
138
|
-
- `gamealgo events count` 能查到测试事件。
|
|
139
|
-
- 配置未就绪时,游戏仍然能走本地默认逻辑。
|
|
140
|
-
- Report Pack 可以 `validate` 和 `preview`。
|
|
141
|
-
|
|
142
|
-
如果以上验证没有完成,交付说明里必须标记为 Scaffolded 或部分完成,不能写“已完成接入”。
|
|
67
|
+
```bash
|
|
68
|
+
npm install
|
|
69
|
+
npm run check
|
|
70
|
+
```
|
|
143
71
|
|
|
144
|
-
|
|
72
|
+
平台 SDK README 只描述与当前代码版本绑定的安装方式、公开 API 和运行时约束。业务流程与平台规则以 `gamealgo docs` 返回的在线文档为准。
|
package/cli/README.md
CHANGED
|
@@ -159,35 +159,47 @@ gamealgo experiment strategy publish strategy.yaml --yes
|
|
|
159
159
|
|
|
160
160
|
每次发布 Strategy 都必须显式填写 `requiredIntegrationVersion`。填写 `0` 表示该 Strategy 不依赖任何客户端实验能力版本;填写大于 `0` 的值时,必须引用已经通过 `integration-version create` 创建、且客户端确实已经实现的版本。Run 会继承 Strategy 的最低版本,不需要重复配置。
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
版本升级判断、Strategy 最低版本和托管实验覆盖率门槛的完整说明可通过 `gamealgo docs experiment-integration-versions` 获取。
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
默认创建托管实验 Run。除非开发者具备专业的实验设计和统计判断能力,并且会持续人工管理实验,否则不要使用手动实验:
|
|
165
165
|
|
|
166
166
|
```yaml
|
|
167
167
|
# run.yaml
|
|
168
|
-
displayName:
|
|
169
|
-
type: manual
|
|
168
|
+
displayName: 广告频率托管实验
|
|
170
169
|
platform: ios
|
|
171
170
|
objectiveTemplate: ltv_proxy@1
|
|
172
171
|
baselineVariantId: control
|
|
172
|
+
minWindowDays: 7
|
|
173
|
+
maxWindowDays: 14
|
|
174
|
+
maxVariantsPerRound: 2
|
|
173
175
|
variants:
|
|
174
176
|
- variantId: control
|
|
175
177
|
weight: 1
|
|
176
178
|
config:
|
|
177
179
|
firstAdLevel: 4
|
|
178
180
|
interval: 30
|
|
181
|
+
script:
|
|
182
|
+
versionId: sv_xxxxxxxxxxxxxxxx
|
|
179
183
|
- variantId: slower_ads
|
|
180
184
|
weight: 1
|
|
181
185
|
config:
|
|
182
186
|
firstAdLevel: 5
|
|
183
187
|
interval: 45
|
|
188
|
+
script:
|
|
189
|
+
versionId: sv_xxxxxxxxxxxxxxxx
|
|
184
190
|
```
|
|
185
191
|
|
|
186
192
|
```bash
|
|
187
193
|
gamealgo experiment run create ad_frequency run.yaml --yes
|
|
188
194
|
```
|
|
189
195
|
|
|
190
|
-
|
|
196
|
+
CLI 在未声明 `type` 时默认提交 `managed`。托管实验由平台管理轮次、在参考周期后判断是否需要延长,并在证据明确时完成后续轮次和推全;缺少 `minWindowDays` 或 `maxWindowDays` 会直接拒绝创建,不会静默套用周期。
|
|
197
|
+
|
|
198
|
+
Run 不会继承 Strategy 的脚本。每个需要执行脚本的 variant 都必须显式配置 `script.versionId`;省略 `script` 明确表示该组是 config-only。即使多个 variant 使用同一份脚本,也要在每个 variant 上重复填写同一个不可变 `versionId`。
|
|
199
|
+
|
|
200
|
+
如果要把当前默认参数也放进实验,需要显式配置 `variantId: default`。`default` 会快照 Strategy 当前默认参数,因此不能写 `config`;如果默认组需要执行脚本,也必须在 `default` 上显式写 `script.versionId`。否则默认参数不参与流量。所有有流量的实验组数量必须至少为 2。
|
|
201
|
+
|
|
202
|
+
Run 创建后,用户不能修改 variant、权重或启停状态,否则同一用户可能换组,前后数据也无法按同一实验口径比较。托管实验只允许平台在轮次结束后按既定流程切换下一轮候选,用户不能干预流量。需要改变候选或分流时,先取消当前 Run,再创建新 Run。
|
|
191
203
|
|
|
192
204
|
查看、评估和完成实验闭环:
|
|
193
205
|
|
|
@@ -201,12 +213,44 @@ gamealgo experiment run report xrun_xxxxxxxxxxxxxxxx
|
|
|
201
213
|
gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant slower_ads --message "采用低频广告组" --yes
|
|
202
214
|
```
|
|
203
215
|
|
|
204
|
-
`baselineVariantId` 是本次评估的对照组,必须指向一个有流量的实验组。平台按小时更新“当前评估”;`report` 返回已生成的正式报告。`promote`
|
|
216
|
+
`baselineVariantId` 是本次评估的对照组,必须指向一个有流量的实验组。平台按小时更新“当前评估”;`report` 返回已生成的正式报告。`promote` 可在手动或托管 Run 仍处于运行中时提前人工推全,不要求平台已经给出正式 winner。它会立即结束 Run,把指定 variant 的 `config` / `script` 写回 Strategy 默认参数,并淘汰其他实验组;必须通过 `--message` 记录人工推全原因。取消实验用:
|
|
217
|
+
|
|
218
|
+
`objectiveTemplate` 必须显式选择:
|
|
219
|
+
|
|
220
|
+
- `ltv_proxy@1`:优化标准化累计用户价值。
|
|
221
|
+
- `active_days@1`:优化用户活跃天数,口径为 `1 + D1 回访率 + ... + Dk 回访率`。D0 记为 1 个活跃日,每个 Dx 只使用已经成熟到该天的实验进入用户,未回访用户按 0 计入。
|
|
222
|
+
|
|
223
|
+
实验结束前可以显式修改追踪目标。修改不会重置实验分流、时间窗口、参数或脚本;平台会把当前评估标记为过期,并在下一轮评估中使用新目标重算已有实验窗口。托管实验一旦已有完成的轮次就不能修改目标,因为前后轮次不能使用不同的优选依据。
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
gamealgo experiment run objective xrun_xxxxxxxxxxxxxxxx \
|
|
227
|
+
--objective active_days@1 \
|
|
228
|
+
--message "改为关注用户活跃天数" \
|
|
229
|
+
--yes
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
已生成的正式轮次报告保留生成时的旧目标,不会被改写。
|
|
205
233
|
|
|
206
234
|
```bash
|
|
207
235
|
gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --message "实验停止" --yes
|
|
208
236
|
```
|
|
209
237
|
|
|
238
|
+
### 高级用法:手动实验
|
|
239
|
+
|
|
240
|
+
手动实验不会自动延长、形成最终结论或推全。只有开发者能够自行决定采集周期、判断统计证据并负责完成实验闭环时才使用。必须通过以下任一方式显式开启:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
gamealgo experiment run create ad_frequency manual-run.yaml --manual --yes
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
或者在 Run 文件中明确填写:
|
|
247
|
+
|
|
248
|
+
```yaml
|
|
249
|
+
type: manual
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
手动 Run 不配置 `minWindowDays`、`maxWindowDays` 和 `startCondition`。其分流同样在整个 Run 内固定;需要调整分流时,取消当前 Run 后重新创建。
|
|
253
|
+
|
|
210
254
|
回滚默认参数会创建一个新的参数版本,不会改写历史版本:
|
|
211
255
|
|
|
212
256
|
```bash
|
|
@@ -221,28 +265,13 @@ gamealgo experiment override list xrun_xxxxxxxxxxxxxxxx
|
|
|
221
265
|
gamealgo experiment override delete xrun_xxxxxxxxxxxxxxxx --user user-1 --yes
|
|
222
266
|
```
|
|
223
267
|
|
|
224
|
-
|
|
268
|
+
托管实验可以使用 `startCondition` 等待接入版本覆盖率和用户数达到门槛:
|
|
225
269
|
|
|
226
270
|
```yaml
|
|
227
|
-
displayName: 广告频率托管实验
|
|
228
|
-
type: managed
|
|
229
|
-
platform: ios
|
|
230
|
-
objectiveTemplate: ltv_proxy@1
|
|
231
|
-
baselineVariantId: alpha
|
|
232
|
-
minWindowDays: 7
|
|
233
|
-
maxWindowDays: 14
|
|
234
|
-
maxVariantsPerRound: 3
|
|
235
271
|
# 可选:仅托管实验支持。最近 24 小时同时满足两个门槛后才开始首轮。
|
|
236
272
|
startCondition:
|
|
237
273
|
minCoverage: 0.8
|
|
238
274
|
minUsers: 100
|
|
239
|
-
variants:
|
|
240
|
-
- variantId: alpha
|
|
241
|
-
config: { firstAdLevel: 4, interval: 30 }
|
|
242
|
-
- variantId: bravo
|
|
243
|
-
config: { firstAdLevel: 5, interval: 30 }
|
|
244
|
-
- variantId: charlie
|
|
245
|
-
config: { firstAdLevel: 4, interval: 45 }
|
|
246
275
|
```
|
|
247
276
|
|
|
248
277
|
`minCoverage` 表示指定平台最近 24 小时内,实验接入版本达到 Strategy `requiredIntegrationVersion` 的去重用户占比;`minUsers` 表示同一窗口内的非 Debug 去重用户数。两项可单独配置,同时配置时必须同时满足。等待期间 Strategy 继续下发默认参数,满足门槛后平台才生成首轮时间窗并开始分流。手动实验不支持 `startCondition`,托管实验不配置时仍会立即启动。`experiment run show` 返回 `startConditionSnapshot`,可查看当前用户数、覆盖率和最近检查时间。
|
|
@@ -259,6 +288,8 @@ LTV Proxy (Dk) = STANDARDIZED_DAY_VALUE_D0 + ... + STANDARDIZED_DAY_VALUE_Dk
|
|
|
259
288
|
|
|
260
289
|
所有实验组使用同一个 `k`。平台按参考周期固定 `k 上限 = min(14, minWindowDays - 3)`,再选择不超过上限、且每个参与组都至少有 10 个成熟进入用户的最大连续 Dx。延长只增加样本,不改变评分口径。平台用本轮真实用户和线上 MurmurHash 分流规则模拟大量随机 seed,估计当前游戏用户结构下的自然组间波动,并校正多实验组与逐日重复评估。实际收入、实际 ARPU、新用户留存和新用户 LTV 只作为诊断指标,不参与主指标计算。
|
|
261
290
|
|
|
291
|
+
`active_days@1` 使用相同的共同成熟 `k`、随机化检验和正式胜出规则,收入和 LTV 指标仍作为诊断数据展示,但不参与主指标排序。
|
|
292
|
+
|
|
262
293
|
所有会修改线上状态的实验命令都必须显式传 `--yes`。
|
|
263
294
|
|
|
264
295
|
## 脚本和配置
|
package/dist/gamealgo.js
CHANGED
|
@@ -387,18 +387,37 @@ async function handleExperimentRun(client, args, global) {
|
|
|
387
387
|
const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
|
|
388
388
|
const filePath = optionalString(flags.file || flags.input) || optionalString(args.shift());
|
|
389
389
|
if (!strategyKey || !filePath)
|
|
390
|
-
throw new Error("usage: gamealgo experiment run create <strategyKey> <run.yaml> --yes");
|
|
390
|
+
throw new Error("usage: gamealgo experiment run create <strategyKey> <run.yaml> [--manual] --yes");
|
|
391
391
|
await requireYes(flags, global, "experiment run create");
|
|
392
392
|
const body = await readObjectFile(filePath, "experiment run");
|
|
393
393
|
if (!optionalString(body.displayName))
|
|
394
394
|
throw new Error("experiment run requires displayName");
|
|
395
|
-
|
|
396
|
-
|
|
395
|
+
const objectiveTemplate = optionalString(body.objectiveTemplate);
|
|
396
|
+
if (!objectiveTemplate || !["ltv_proxy@1", "active_days@1"].includes(objectiveTemplate)) {
|
|
397
|
+
throw new Error("experiment run requires objectiveTemplate: ltv_proxy@1 or active_days@1");
|
|
397
398
|
}
|
|
398
399
|
if (!optionalString(body.baselineVariantId)) {
|
|
399
400
|
throw new Error("experiment run requires explicit baselineVariantId");
|
|
400
401
|
}
|
|
401
|
-
|
|
402
|
+
validateExperimentRunVariantScripts(body.variants);
|
|
403
|
+
const fileRunType = optionalString(body.type);
|
|
404
|
+
const flagRunType = optionalString(flags.type);
|
|
405
|
+
const manualFlag = flags.manual === true;
|
|
406
|
+
if (fileRunType && fileRunType !== "managed" && fileRunType !== "manual") {
|
|
407
|
+
throw new Error("experiment run type must be managed or manual");
|
|
408
|
+
}
|
|
409
|
+
if (flagRunType && flagRunType !== "managed" && flagRunType !== "manual") {
|
|
410
|
+
throw new Error("--type must be managed or manual");
|
|
411
|
+
}
|
|
412
|
+
if (manualFlag && flagRunType && flagRunType !== "manual") {
|
|
413
|
+
throw new Error("--manual conflicts with --type managed");
|
|
414
|
+
}
|
|
415
|
+
const requestedRunType = manualFlag ? "manual" : flagRunType;
|
|
416
|
+
if (requestedRunType && fileRunType && requestedRunType !== fileRunType) {
|
|
417
|
+
throw new Error(`experiment run type conflict: file uses ${fileRunType}, CLI requests ${requestedRunType}`);
|
|
418
|
+
}
|
|
419
|
+
const runType = requestedRunType || fileRunType || "managed";
|
|
420
|
+
body.type = runType;
|
|
402
421
|
if (runType !== "managed" && body.startCondition !== undefined) {
|
|
403
422
|
throw new Error("experiment run startCondition is supported only when type is managed");
|
|
404
423
|
}
|
|
@@ -412,7 +431,13 @@ async function handleExperimentRun(client, args, global) {
|
|
|
412
431
|
throw new Error("managed experiment run requires maxWindowDays between minWindowDays and 60");
|
|
413
432
|
}
|
|
414
433
|
}
|
|
415
|
-
|
|
434
|
+
else {
|
|
435
|
+
process.stderr.write("Warning: creating a manual experiment. The platform will not automatically extend, conclude, or promote it; use manual mode only when the developer will manage the experiment decision.\n");
|
|
436
|
+
}
|
|
437
|
+
const result = await client.createExperimentV2Run(strategyKey, body);
|
|
438
|
+
await printResult(result, global);
|
|
439
|
+
if (!global.json)
|
|
440
|
+
printExperimentRunVariantScripts(result);
|
|
416
441
|
return;
|
|
417
442
|
}
|
|
418
443
|
if (sub === "show" || sub === "get") {
|
|
@@ -499,6 +524,21 @@ async function handleExperimentRun(client, args, global) {
|
|
|
499
524
|
await printResult({ runId, report }, global);
|
|
500
525
|
return;
|
|
501
526
|
}
|
|
527
|
+
if (sub === "objective") {
|
|
528
|
+
const flags = parseFlags(args);
|
|
529
|
+
const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
|
|
530
|
+
const objectiveTemplate = optionalString(flags.objective || flags["objective-template"]);
|
|
531
|
+
if (!runId || !objectiveTemplate) {
|
|
532
|
+
throw new Error("usage: gamealgo experiment run objective <runId> --objective <ltv_proxy@1|active_days@1> --message <reason> --yes");
|
|
533
|
+
}
|
|
534
|
+
if (!["ltv_proxy@1", "active_days@1"].includes(objectiveTemplate)) {
|
|
535
|
+
throw new Error("experiment objective must be ltv_proxy@1 or active_days@1");
|
|
536
|
+
}
|
|
537
|
+
const message = requiredMessage(flags, "experiment run objective");
|
|
538
|
+
await requireYes(flags, global, "experiment run objective");
|
|
539
|
+
await printResult(await client.updateExperimentV2RunObjective(runId, objectiveTemplate, message), global);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
502
542
|
if (sub === "promote") {
|
|
503
543
|
const flags = parseFlags(args);
|
|
504
544
|
const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
|
|
@@ -520,23 +560,7 @@ async function handleExperimentRun(client, args, global) {
|
|
|
520
560
|
await printResult(await client.cancelExperimentV2Run(runId, message), global);
|
|
521
561
|
return;
|
|
522
562
|
}
|
|
523
|
-
|
|
524
|
-
const flags = parseFlags(args);
|
|
525
|
-
const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
|
|
526
|
-
const variantId = optionalString(flags.variant || flags["variant-id"]) || optionalString(args.shift());
|
|
527
|
-
if (!runId || !variantId)
|
|
528
|
-
throw new Error("usage: gamealgo experiment run traffic <runId> <variantId> [--weight n] [--status running|paused] --message <reason> --yes");
|
|
529
|
-
const message = requiredMessage(flags, "experiment run traffic");
|
|
530
|
-
await requireYes(flags, global, "experiment run traffic");
|
|
531
|
-
const body = { message };
|
|
532
|
-
if (flags.weight !== undefined)
|
|
533
|
-
body.weight = optionalNonNegativeInteger(flags.weight, "weight");
|
|
534
|
-
if (flags.status !== undefined)
|
|
535
|
-
body.status = optionalString(flags.status);
|
|
536
|
-
await printResult(await client.updateExperimentV2VariantTraffic(runId, variantId, body), global);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
throw new Error("usage: gamealgo experiment run <create|show|status|evaluate|report|promote|cancel|traffic>");
|
|
563
|
+
throw new Error("usage: gamealgo experiment run <create|show|status|evaluate|report|objective|promote|cancel>");
|
|
540
564
|
}
|
|
541
565
|
function requiredMessage(flags, action) {
|
|
542
566
|
const message = optionalString(flags.message);
|
|
@@ -1082,6 +1106,12 @@ class GameAlgoAdminClient {
|
|
|
1082
1106
|
async cancelExperimentV2Run(runId, message) {
|
|
1083
1107
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/cancel`, { message });
|
|
1084
1108
|
}
|
|
1109
|
+
async updateExperimentV2RunObjective(runId, objectiveTemplate, message) {
|
|
1110
|
+
return await this.patch(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/objective`, {
|
|
1111
|
+
objectiveTemplate,
|
|
1112
|
+
message,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1085
1115
|
async listExperimentV2Overrides(runId) {
|
|
1086
1116
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/overrides`);
|
|
1087
1117
|
}
|
|
@@ -1091,9 +1121,6 @@ class GameAlgoAdminClient {
|
|
|
1091
1121
|
async deleteExperimentV2Override(runId, userId) {
|
|
1092
1122
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/overrides/delete`, { userId });
|
|
1093
1123
|
}
|
|
1094
|
-
async updateExperimentV2VariantTraffic(runId, variantId, body) {
|
|
1095
|
-
return await this.patch(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/variants/${encodeURIComponent(variantId)}/traffic`, body);
|
|
1096
|
-
}
|
|
1097
1124
|
async listConfigFiles() {
|
|
1098
1125
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/config-files`);
|
|
1099
1126
|
}
|
|
@@ -1489,6 +1516,35 @@ function latestScriptVersionNames(value) {
|
|
|
1489
1516
|
function objectValue(value) {
|
|
1490
1517
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1491
1518
|
}
|
|
1519
|
+
function validateExperimentRunVariantScripts(value) {
|
|
1520
|
+
if (!Array.isArray(value))
|
|
1521
|
+
throw new Error("experiment run requires variants array");
|
|
1522
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1523
|
+
const variant = objectValue(value[index]);
|
|
1524
|
+
if (!variant)
|
|
1525
|
+
throw new Error(`variants[${index}] must be an object`);
|
|
1526
|
+
if (!Object.prototype.hasOwnProperty.call(variant, "script"))
|
|
1527
|
+
continue;
|
|
1528
|
+
const script = objectValue(variant.script);
|
|
1529
|
+
if (!script || !optionalString(script.versionId)) {
|
|
1530
|
+
throw new Error(`variants[${index}].script requires versionId; omit script for a config-only variant`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function printExperimentRunVariantScripts(value) {
|
|
1535
|
+
const run = objectValue(objectValue(value)?.run);
|
|
1536
|
+
if (!run || !Array.isArray(run.variants))
|
|
1537
|
+
return;
|
|
1538
|
+
console.log("\nVariant execution:");
|
|
1539
|
+
for (const item of run.variants) {
|
|
1540
|
+
const variant = objectValue(item);
|
|
1541
|
+
if (!variant)
|
|
1542
|
+
continue;
|
|
1543
|
+
const variantId = optionalString(variant.variantId) ?? "(unknown)";
|
|
1544
|
+
const versionId = optionalString(objectValue(variant.script)?.versionId);
|
|
1545
|
+
console.log(` ${variantId}: ${versionId ? `script ${versionId}` : "config-only"}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1492
1548
|
async function readJsonFile(filePath) {
|
|
1493
1549
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
1494
1550
|
}
|
|
@@ -1675,14 +1731,14 @@ Usage:
|
|
|
1675
1731
|
gamealgo experiment strategy default ad_frequency strategy.yaml --yes
|
|
1676
1732
|
gamealgo experiment strategy archive ad_frequency --yes
|
|
1677
1733
|
gamealgo experiment strategy rollback ad_frequency --version-id xv_xxxxxxxxxxxxxxxx --message "回滚参数" --yes
|
|
1678
|
-
gamealgo experiment run create ad_frequency run.yaml --yes
|
|
1734
|
+
gamealgo experiment run create ad_frequency run.yaml [--manual] --yes
|
|
1679
1735
|
gamealgo experiment run show xrun_xxxxxxxxxxxxxxxx
|
|
1680
1736
|
gamealgo experiment run status xrun_xxxxxxxxxxxxxxxx
|
|
1681
1737
|
gamealgo experiment run evaluate xrun_xxxxxxxxxxxxxxxx --from 2026-07-01 --to 2026-07-07 --yes
|
|
1682
1738
|
gamealgo experiment run report xrun_xxxxxxxxxxxxxxxx --round 1
|
|
1739
|
+
gamealgo experiment run objective xrun_xxxxxxxxxxxxxxxx --objective active_days@1 --message "改为关注活跃天数" --yes
|
|
1683
1740
|
gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant bravo --message "采用胜出组" --yes
|
|
1684
1741
|
gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --message "停止实验" --yes
|
|
1685
|
-
gamealgo experiment run traffic xrun_xxxxxxxxxxxxxxxx bravo --weight 0 --status paused --message "暂停异常组" --yes
|
|
1686
1742
|
gamealgo experiment override set xrun_xxxxxxxxxxxxxxxx --user user-1 --variant bravo --yes
|
|
1687
1743
|
gamealgo experiment override list xrun_xxxxxxxxxxxxxxxx
|
|
1688
1744
|
|