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 CHANGED
@@ -1,144 +1,72 @@
1
1
  # GameAlgo Client
2
2
 
3
- GameAlgo Client 是面向游戏接入方的公开客户端仓库,包含 iOS SDK、Android SDK、REST helper、TapTap Maker / Lua SDK、CLI 和接入文档。
3
+ GameAlgo Client 是公开客户端仓库,包含 iOS SDK、Android SDK、REST helper、TapTap Maker / Lua SDK、CLI 和协议定义。
4
4
 
5
- ## 如果你是 AI Agent,请从这里开始
5
+ 产品能力、接入流程、埋点、实验、报表与优化方法由 GameAlgo Server 在线文档统一维护,不在本仓库保留副本。
6
6
 
7
- 你的目标不是把 README 转发给开发者,而是直接帮助当前游戏完成 GameAlgo 接入、验证和后续报表/实验配置。
7
+ ## AI Agent 从这里开始
8
8
 
9
- 默认目标是 Activated 接入,而不是只把 SDK 代码接进工程。
9
+ 不要把 README 原样转发给开发者。先安装最新 CLI,再从目标环境读取当前文档并完成接入。
10
10
 
11
- | 状态 | 含义 | 能否声称接入完成 |
11
+ | 环境 | Admin host | SDK / REST API |
12
12
  | --- | --- | --- |
13
- | Activated | 已使用真实 `ga_live_*`,能拉配置,并能通过事件统计看到测试事件 | 可以 |
14
- | Scaffolded | SDK 代码已接入,但没有真实 `ga_live_*`,运行时仍是占位或 no-op | 不可以 |
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
- 安装和更新 CLI 都使用 `npm install -g gamealgo-cli@latest`,不需要拉取 GameAlgo Client 仓库。请在每个游戏项目根目录执行 `gamealgo login`,凭据保存在项目的 `.gamealgo/cli.json`,不同游戏互不覆盖。`gamealgo docs` 从当前 Admin Host 获取最新 AI 文档;登录前显式传 `--host`,登录后 CLI 会使用已保存的 Host。需要网页阅读时使用 `gamealgo docs --open`。
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
- 1. 登录 GameAlgo Admin 控制台。
79
- 2. 创建或选择你的游戏。
80
- 3. 在游戏的 Keys 页面创建 Game Admin Key。
81
- 4. 把使用环境和 Game Admin Key 提供给 AI Agent。
82
- 5. 审核 AI Agent 对游戏工程、实验配置和 Report Pack 的改动。
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
- ## 两类 key
42
+ ## SDK 入口
87
43
 
88
- | Key | 示例 | 用途 | 放在哪里 |
89
- | --- | --- | --- | --- |
90
- | Game Admin Key | `ga_admin_*` | CLI / AI Agent / CI 管理实验、脚本、配置、Report Pack,拉取报表和事件统计 | 开发机器、CI Secret、Agent Secret |
91
- | Client Game Key | `ga_live_*` | 游戏运行时拉取配置、拉取配置文件、上报事件 | 游戏客户端 SDK 配置 |
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
- 常规流程是:开发者只手工创建 Game Admin Key;AI Agent 用 CLI 创建、读取和维护 Client Game Key。
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 / TapTap 小游戏 Lua SDK 和客户端 HTTP 传输
102
- cli/ 游戏开发 Agent 使用的自动化 CLI
103
- examples/ 可运行接入示例
104
- docs/ 接入、埋点、报表和优化文档
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
- - iOS:见 [iOS SDK](./ios/README.md),也可以通过 SwiftPM 引入本仓库。
121
- - Android:见 [Android SDK](./android/README.md)。
122
- - REST / Web / Backend:见 [REST helper](./rest-api/README.md) 和 [REST API 文档](./docs/rest-api-v1.md)。
123
- - TapTap Maker / TapTap 小游戏:见 [Lua SDK](./lua/README.md)。
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
- 完整验收和排查流程见 [AI 接入流程](./docs/agent-onboarding.md)。
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
- 版本升级判断、旧客户端行为和托管实验覆盖率门槛的完整说明见 [实验接入版本](../docs/experiment-integration-versions.md)。
162
+ 版本升级判断、Strategy 最低版本和托管实验覆盖率门槛的完整说明可通过 `gamealgo docs experiment-integration-versions` 获取。
163
163
 
164
- 创建手动实验 Run:
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
- 如果要把当前默认参数也放进实验,需要显式配置 `variantId: default`;否则默认参数不参与流量。所有有流量的实验组数量必须至少为 2。
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` 会把指定 variant 写回 Strategy 默认参数,并结束当前 Run。取消实验用:
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
- 托管实验也是创建 `type: managed` 的 Run,平台会按轮次自动调整流量并最终推全胜出组:
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
- if (optionalString(body.objectiveTemplate) !== "ltv_proxy@1") {
396
- throw new Error("experiment run requires objectiveTemplate: ltv_proxy@1");
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
- const runType = optionalString(body.type) || "manual";
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
- await printResult(await client.createExperimentV2Run(strategyKey, body), global);
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
- if (sub === "traffic") {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gamealgo-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "GameAlgo command-line client for AI agents and game developers",
5
5
  "type": "module",
6
6
  "bin": {