gamealgo-cli 0.1.3 → 0.1.5
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 +36 -6
- package/dist/gamealgo.js +72 -5
- 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,7 +159,7 @@ 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
|
|
|
@@ -176,18 +176,24 @@ variants:
|
|
|
176
176
|
config:
|
|
177
177
|
firstAdLevel: 4
|
|
178
178
|
interval: 30
|
|
179
|
+
script:
|
|
180
|
+
versionId: sv_xxxxxxxxxxxxxxxx
|
|
179
181
|
- variantId: slower_ads
|
|
180
182
|
weight: 1
|
|
181
183
|
config:
|
|
182
184
|
firstAdLevel: 5
|
|
183
185
|
interval: 45
|
|
186
|
+
script:
|
|
187
|
+
versionId: sv_xxxxxxxxxxxxxxxx
|
|
184
188
|
```
|
|
185
189
|
|
|
186
190
|
```bash
|
|
187
191
|
gamealgo experiment run create ad_frequency run.yaml --yes
|
|
188
192
|
```
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
Run 不会继承 Strategy 的脚本。每个需要执行脚本的 variant 都必须显式配置 `script.versionId`;省略 `script` 明确表示该组是 config-only。即使多个 variant 使用同一份脚本,也要在每个 variant 上重复填写同一个不可变 `versionId`。
|
|
195
|
+
|
|
196
|
+
如果要把当前默认参数也放进实验,需要显式配置 `variantId: default`。`default` 会快照 Strategy 当前默认参数,因此不能写 `config`;如果默认组需要执行脚本,也必须在 `default` 上显式写 `script.versionId`。否则默认参数不参与流量。所有有流量的实验组数量必须至少为 2。
|
|
191
197
|
|
|
192
198
|
查看、评估和完成实验闭环:
|
|
193
199
|
|
|
@@ -203,6 +209,22 @@ gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant slower_ads --mes
|
|
|
203
209
|
|
|
204
210
|
`baselineVariantId` 是本次评估的对照组,必须指向一个有流量的实验组。平台按小时更新“当前评估”;`report` 返回已生成的正式报告。`promote` 会把指定 variant 写回 Strategy 默认参数,并结束当前 Run。取消实验用:
|
|
205
211
|
|
|
212
|
+
`objectiveTemplate` 必须显式选择:
|
|
213
|
+
|
|
214
|
+
- `ltv_proxy@1`:优化标准化累计用户价值。
|
|
215
|
+
- `active_days@1`:优化用户活跃天数,口径为 `1 + D1 回访率 + ... + Dk 回访率`。D0 记为 1 个活跃日,每个 Dx 只使用已经成熟到该天的实验进入用户,未回访用户按 0 计入。
|
|
216
|
+
|
|
217
|
+
实验结束前可以显式修改追踪目标。修改不会重置实验分流、时间窗口、参数或脚本;平台会把当前评估标记为过期,并在下一轮评估中使用新目标重算已有实验窗口。托管实验一旦已有完成的轮次就不能修改目标,因为前后轮次不能使用不同的优选依据。
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
gamealgo experiment run objective xrun_xxxxxxxxxxxxxxxx \
|
|
221
|
+
--objective active_days@1 \
|
|
222
|
+
--message "改为关注用户活跃天数" \
|
|
223
|
+
--yes
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
已生成的正式轮次报告保留生成时的旧目标,不会被改写。
|
|
227
|
+
|
|
206
228
|
```bash
|
|
207
229
|
gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --message "实验停止" --yes
|
|
208
230
|
```
|
|
@@ -229,7 +251,8 @@ type: managed
|
|
|
229
251
|
platform: ios
|
|
230
252
|
objectiveTemplate: ltv_proxy@1
|
|
231
253
|
baselineVariantId: alpha
|
|
232
|
-
|
|
254
|
+
minWindowDays: 7
|
|
255
|
+
maxWindowDays: 14
|
|
233
256
|
maxVariantsPerRound: 3
|
|
234
257
|
# 可选:仅托管实验支持。最近 24 小时同时满足两个门槛后才开始首轮。
|
|
235
258
|
startCondition:
|
|
@@ -238,23 +261,30 @@ startCondition:
|
|
|
238
261
|
variants:
|
|
239
262
|
- variantId: alpha
|
|
240
263
|
config: { firstAdLevel: 4, interval: 30 }
|
|
264
|
+
script: { versionId: sv_xxxxxxxxxxxxxxxx }
|
|
241
265
|
- variantId: bravo
|
|
242
266
|
config: { firstAdLevel: 5, interval: 30 }
|
|
267
|
+
script: { versionId: sv_xxxxxxxxxxxxxxxx }
|
|
243
268
|
- variantId: charlie
|
|
244
269
|
config: { firstAdLevel: 4, interval: 45 }
|
|
270
|
+
script: { versionId: sv_xxxxxxxxxxxxxxxx }
|
|
245
271
|
```
|
|
246
272
|
|
|
247
273
|
`minCoverage` 表示指定平台最近 24 小时内,实验接入版本达到 Strategy `requiredIntegrationVersion` 的去重用户占比;`minUsers` 表示同一窗口内的非 Debug 去重用户数。两项可单独配置,同时配置时必须同时满足。等待期间 Strategy 继续下发默认参数,满足门槛后平台才生成首轮时间窗并开始分流。手动实验不支持 `startCondition`,托管实验不配置时仍会立即启动。`experiment run show` 返回 `startConditionSnapshot`,可查看当前用户数、覆盖率和最近检查时间。
|
|
248
274
|
|
|
249
|
-
|
|
275
|
+
托管实验只执行候选组数量决定的计划轮次,不会自动增加复赛轮次。每轮至少运行 `minWindowDays` 天;达到参考周期后若证据仍不明确,平台逐日延长,最长运行到 `maxWindowDays`。`minWindowDays` 支持 1~30 天,`maxWindowDays` 支持到 60 天且不能小于参考周期。达到最长周期仍无明确胜出组时结束本轮,Strategy 默认参数保持不变。
|
|
250
276
|
|
|
251
277
|
`ltv_proxy@1` 的标准口径为:
|
|
252
278
|
|
|
253
279
|
```text
|
|
254
|
-
LTV Proxy
|
|
280
|
+
LTV Proxy (Dk) = STANDARDIZED_DAY_VALUE_D0 + ... + STANDARDIZED_DAY_VALUE_Dk
|
|
255
281
|
```
|
|
256
282
|
|
|
257
|
-
|
|
283
|
+
`STANDARDIZED_DAY_VALUE_Dx` 使用所有在 Dx 已成熟的实验进入用户作为分母,未回访用户当日价值按 0 计入。广告曝光按广告类型和用户本地日内曝光序号分桶,每个桶使用本轮所有参与组在实验窗口内共同计算的参考 CPM,再加实际内购收入。
|
|
284
|
+
|
|
285
|
+
所有实验组使用同一个 `k`。平台按参考周期固定 `k 上限 = min(14, minWindowDays - 3)`,再选择不超过上限、且每个参与组都至少有 10 个成熟进入用户的最大连续 Dx。延长只增加样本,不改变评分口径。平台用本轮真实用户和线上 MurmurHash 分流规则模拟大量随机 seed,估计当前游戏用户结构下的自然组间波动,并校正多实验组与逐日重复评估。实际收入、实际 ARPU、新用户留存和新用户 LTV 只作为诊断指标,不参与主指标计算。
|
|
286
|
+
|
|
287
|
+
`active_days@1` 使用相同的共同成熟 `k`、随机化检验和正式胜出规则,收入和 LTV 指标仍作为诊断数据展示,但不参与主指标排序。
|
|
258
288
|
|
|
259
289
|
所有会修改线上状态的实验命令都必须显式传 `--yes`。
|
|
260
290
|
|
package/dist/gamealgo.js
CHANGED
|
@@ -392,16 +392,32 @@ async function handleExperimentRun(client, args, global) {
|
|
|
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 runType = optionalString(body.type) || "manual";
|
|
404
|
+
if (runType !== "managed" && body.startCondition !== undefined) {
|
|
402
405
|
throw new Error("experiment run startCondition is supported only when type is managed");
|
|
403
406
|
}
|
|
404
|
-
|
|
407
|
+
if (runType === "managed") {
|
|
408
|
+
const minWindowDays = Number(body.minWindowDays);
|
|
409
|
+
const maxWindowDays = Number(body.maxWindowDays);
|
|
410
|
+
if (!Number.isInteger(minWindowDays) || minWindowDays < 1 || minWindowDays > 30) {
|
|
411
|
+
throw new Error("managed experiment run requires minWindowDays between 1 and 30");
|
|
412
|
+
}
|
|
413
|
+
if (!Number.isInteger(maxWindowDays) || maxWindowDays < minWindowDays || maxWindowDays > 60) {
|
|
414
|
+
throw new Error("managed experiment run requires maxWindowDays between minWindowDays and 60");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const result = await client.createExperimentV2Run(strategyKey, body);
|
|
418
|
+
await printResult(result, global);
|
|
419
|
+
if (!global.json)
|
|
420
|
+
printExperimentRunVariantScripts(result);
|
|
405
421
|
return;
|
|
406
422
|
}
|
|
407
423
|
if (sub === "show" || sub === "get") {
|
|
@@ -488,6 +504,21 @@ async function handleExperimentRun(client, args, global) {
|
|
|
488
504
|
await printResult({ runId, report }, global);
|
|
489
505
|
return;
|
|
490
506
|
}
|
|
507
|
+
if (sub === "objective") {
|
|
508
|
+
const flags = parseFlags(args);
|
|
509
|
+
const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
|
|
510
|
+
const objectiveTemplate = optionalString(flags.objective || flags["objective-template"]);
|
|
511
|
+
if (!runId || !objectiveTemplate) {
|
|
512
|
+
throw new Error("usage: gamealgo experiment run objective <runId> --objective <ltv_proxy@1|active_days@1> --message <reason> --yes");
|
|
513
|
+
}
|
|
514
|
+
if (!["ltv_proxy@1", "active_days@1"].includes(objectiveTemplate)) {
|
|
515
|
+
throw new Error("experiment objective must be ltv_proxy@1 or active_days@1");
|
|
516
|
+
}
|
|
517
|
+
const message = requiredMessage(flags, "experiment run objective");
|
|
518
|
+
await requireYes(flags, global, "experiment run objective");
|
|
519
|
+
await printResult(await client.updateExperimentV2RunObjective(runId, objectiveTemplate, message), global);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
491
522
|
if (sub === "promote") {
|
|
492
523
|
const flags = parseFlags(args);
|
|
493
524
|
const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
|
|
@@ -525,7 +556,7 @@ async function handleExperimentRun(client, args, global) {
|
|
|
525
556
|
await printResult(await client.updateExperimentV2VariantTraffic(runId, variantId, body), global);
|
|
526
557
|
return;
|
|
527
558
|
}
|
|
528
|
-
throw new Error("usage: gamealgo experiment run <create|show|status|evaluate|report|promote|cancel|traffic>");
|
|
559
|
+
throw new Error("usage: gamealgo experiment run <create|show|status|evaluate|report|objective|promote|cancel|traffic>");
|
|
529
560
|
}
|
|
530
561
|
function requiredMessage(flags, action) {
|
|
531
562
|
const message = optionalString(flags.message);
|
|
@@ -1071,6 +1102,12 @@ class GameAlgoAdminClient {
|
|
|
1071
1102
|
async cancelExperimentV2Run(runId, message) {
|
|
1072
1103
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/cancel`, { message });
|
|
1073
1104
|
}
|
|
1105
|
+
async updateExperimentV2RunObjective(runId, objectiveTemplate, message) {
|
|
1106
|
+
return await this.patch(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/objective`, {
|
|
1107
|
+
objectiveTemplate,
|
|
1108
|
+
message,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1074
1111
|
async listExperimentV2Overrides(runId) {
|
|
1075
1112
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/overrides`);
|
|
1076
1113
|
}
|
|
@@ -1478,6 +1515,35 @@ function latestScriptVersionNames(value) {
|
|
|
1478
1515
|
function objectValue(value) {
|
|
1479
1516
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1480
1517
|
}
|
|
1518
|
+
function validateExperimentRunVariantScripts(value) {
|
|
1519
|
+
if (!Array.isArray(value))
|
|
1520
|
+
throw new Error("experiment run requires variants array");
|
|
1521
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1522
|
+
const variant = objectValue(value[index]);
|
|
1523
|
+
if (!variant)
|
|
1524
|
+
throw new Error(`variants[${index}] must be an object`);
|
|
1525
|
+
if (!Object.prototype.hasOwnProperty.call(variant, "script"))
|
|
1526
|
+
continue;
|
|
1527
|
+
const script = objectValue(variant.script);
|
|
1528
|
+
if (!script || !optionalString(script.versionId)) {
|
|
1529
|
+
throw new Error(`variants[${index}].script requires versionId; omit script for a config-only variant`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function printExperimentRunVariantScripts(value) {
|
|
1534
|
+
const run = objectValue(objectValue(value)?.run);
|
|
1535
|
+
if (!run || !Array.isArray(run.variants))
|
|
1536
|
+
return;
|
|
1537
|
+
console.log("\nVariant execution:");
|
|
1538
|
+
for (const item of run.variants) {
|
|
1539
|
+
const variant = objectValue(item);
|
|
1540
|
+
if (!variant)
|
|
1541
|
+
continue;
|
|
1542
|
+
const variantId = optionalString(variant.variantId) ?? "(unknown)";
|
|
1543
|
+
const versionId = optionalString(objectValue(variant.script)?.versionId);
|
|
1544
|
+
console.log(` ${variantId}: ${versionId ? `script ${versionId}` : "config-only"}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1481
1547
|
async function readJsonFile(filePath) {
|
|
1482
1548
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
1483
1549
|
}
|
|
@@ -1669,6 +1735,7 @@ Usage:
|
|
|
1669
1735
|
gamealgo experiment run status xrun_xxxxxxxxxxxxxxxx
|
|
1670
1736
|
gamealgo experiment run evaluate xrun_xxxxxxxxxxxxxxxx --from 2026-07-01 --to 2026-07-07 --yes
|
|
1671
1737
|
gamealgo experiment run report xrun_xxxxxxxxxxxxxxxx --round 1
|
|
1738
|
+
gamealgo experiment run objective xrun_xxxxxxxxxxxxxxxx --objective active_days@1 --message "改为关注活跃天数" --yes
|
|
1672
1739
|
gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant bravo --message "采用胜出组" --yes
|
|
1673
1740
|
gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --message "停止实验" --yes
|
|
1674
1741
|
gamealgo experiment run traffic xrun_xxxxxxxxxxxxxxxx bravo --weight 0 --status paused --message "暂停异常组" --yes
|