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 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,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
- 版本升级判断、旧客户端行为和托管实验覆盖率门槛的完整说明见 [实验接入版本](../docs/experiment-integration-versions.md)。
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
- 如果要把当前默认参数也放进实验,需要显式配置 `variantId: default`;否则默认参数不参与流量。所有有流量的实验组数量必须至少为 2。
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
- cycleDays: 7
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
- 托管实验只执行候选组数量决定的计划轮次,不会因为结果不显著而自动追加轮次。每轮结束都会生成正式报告;最后一轮只有在证据足够明确时才更新默认参数。没有明确胜出组时,实验正常完成,Strategy 默认参数保持不变。
275
+ 托管实验只执行候选组数量决定的计划轮次,不会自动增加复赛轮次。每轮至少运行 `minWindowDays` 天;达到参考周期后若证据仍不明确,平台逐日延长,最长运行到 `maxWindowDays`。`minWindowDays` 支持 1~30 天,`maxWindowDays` 支持到 60 天且不能小于参考周期。达到最长周期仍无明确胜出组时结束本轮,Strategy 默认参数保持不变。
250
276
 
251
277
  `ltv_proxy@1` 的标准口径为:
252
278
 
253
279
  ```text
254
- LTV Proxy = DAU ARPU * (1 + D1 + D2 + D3 + D4 + D5)
280
+ LTV Proxy (Dk) = STANDARDIZED_DAY_VALUE_D0 + ... + STANDARDIZED_DAY_VALUE_Dk
255
281
  ```
256
282
 
257
- 其中 DAU ARPU 是实验窗口内总收入除以活跃用户天数;Dx 留存只使用截至评估时已经成熟的 cohort,并按 cohort 用户数加权。平台不会等待最后一天用户的 D5 成熟后再结束实验。
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
- 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
- if ((optionalString(body.type) || "manual") !== "managed" && body.startCondition !== undefined) {
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
- await printResult(await client.createExperimentV2Run(strategyKey, body), global);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gamealgo-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "GameAlgo command-line client for AI agents and game developers",
5
5
  "type": "module",
6
6
  "bin": {