gamealgo-cli 0.1.0 → 0.1.2

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
@@ -111,7 +111,7 @@ docs/ 接入、埋点、报表和优化文档
111
111
  | 环境 | SDK / REST API | Admin / CLI |
112
112
  | --- | --- | --- |
113
113
  | 国内 | `https://game-algo-sdk.dictapis.cn` | `https://game-algo-admin.dictapis.cn` |
114
- | 海外 | `https://gamealgo-server-v2.gamealgo-sdk-1ea5b9.workers.dev` | `https://dirichlet.ai/algo_admin` |
114
+ | 海外 | `https://dirichlet.ai/algo_sdk` | `https://dirichlet.ai/algo_admin` |
115
115
 
116
116
  客户端 SDK 只配置 SDK / REST API 地址。Admin / CLI 地址只给开发者、AI Agent 和 CI 使用。
117
117
 
package/cli/README.md CHANGED
@@ -18,6 +18,7 @@ CLI 使用游戏维度的 Game Admin Key 鉴权。这个 key 只绑定一个游
18
18
  ```bash
19
19
  npm install -g gamealgo-cli@latest
20
20
  gamealgo help
21
+ gamealgo --version
21
22
  ```
22
23
 
23
24
  更新 CLI 使用同一条命令:
@@ -28,6 +29,8 @@ npm install -g gamealgo-cli@latest
28
29
 
29
30
  安装和更新不需要拉取 GameAlgo Client 仓库。Node.js 版本需要为 24 或更高。
30
31
 
32
+ Client SDK 源码仓库:<https://github.com/sunyuhao-code/GameAlgoClient>
33
+
31
34
  只有开发 CLI 本身时才需要源码仓库。源码内调试可以用 npm script,但涉及 `--json` 的命令必须加 `--silent`,避免 npm 自己的日志污染 JSON stdout:
32
35
 
33
36
  ```bash
@@ -163,7 +166,8 @@ gamealgo experiment strategy publish strategy.yaml --yes
163
166
  displayName: 广告频率第一轮
164
167
  type: manual
165
168
  platform: ios
166
- objective: ltv_proxy
169
+ objectiveTemplate: ltv_proxy@1
170
+ baselineVariantId: control
167
171
  variants:
168
172
  - variantId: control
169
173
  weight: 1
@@ -189,15 +193,22 @@ gamealgo experiment run create ad_frequency run.yaml --yes
189
193
  gamealgo experiment strategies
190
194
  gamealgo experiment strategy show ad_frequency
191
195
  gamealgo experiment run show xrun_xxxxxxxxxxxxxxxx
196
+ gamealgo experiment run status xrun_xxxxxxxxxxxxxxxx
192
197
  gamealgo experiment run evaluate xrun_xxxxxxxxxxxxxxxx --from 2026-07-01 --to 2026-07-07 --yes
193
198
  gamealgo experiment run report xrun_xxxxxxxxxxxxxxxx
194
- gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant slower_ads --yes
199
+ gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant slower_ads --message "采用低频广告组" --yes
200
+ ```
201
+
202
+ `baselineVariantId` 是本次评估的对照组,必须指向一个有流量的实验组。平台按小时更新“当前评估”;`report` 返回已生成的正式报告。`promote` 会把指定 variant 写回 Strategy 默认参数,并结束当前 Run。取消实验用:
203
+
204
+ ```bash
205
+ gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --message "实验停止" --yes
195
206
  ```
196
207
 
197
- `promote` 会把胜出的 variant 写回 Strategy 默认参数,并结束当前 Run。取消实验用:
208
+ 回滚默认参数会创建一个新的参数版本,不会改写历史版本:
198
209
 
199
210
  ```bash
200
- gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --yes
211
+ gamealgo experiment strategy rollback ad_frequency --version-id xv_xxxxxxxxxxxxxxxx --message "回滚线上参数" --yes
201
212
  ```
202
213
 
203
214
  调试指定用户/设备强制进某个实验组:
@@ -214,7 +225,8 @@ gamealgo experiment override delete xrun_xxxxxxxxxxxxxxxx --user user-1 --yes
214
225
  displayName: 广告频率托管实验
215
226
  type: managed
216
227
  platform: ios
217
- objective: ltv_proxy
228
+ objectiveTemplate: ltv_proxy@1
229
+ baselineVariantId: alpha
218
230
  cycleDays: 7
219
231
  maxVariantsPerRound: 3
220
232
  # 可选:仅托管实验支持。最近 24 小时同时满足两个门槛后才开始首轮。
@@ -232,6 +244,16 @@ variants:
232
244
 
233
245
  `minCoverage` 表示指定平台最近 24 小时内,实验接入版本达到 Strategy `requiredIntegrationVersion` 的去重用户占比;`minUsers` 表示同一窗口内的非 Debug 去重用户数。两项可单独配置,同时配置时必须同时满足。等待期间 Strategy 继续下发默认参数,满足门槛后平台才生成首轮时间窗并开始分流。手动实验不支持 `startCondition`,托管实验不配置时仍会立即启动。`experiment run show` 返回 `startConditionSnapshot`,可查看当前用户数、覆盖率和最近检查时间。
234
246
 
247
+ 托管实验只执行候选组数量决定的计划轮次,不会因为结果不显著而自动追加轮次。每轮结束都会生成正式报告;最后一轮只有在证据足够明确时才更新默认参数。没有明确胜出组时,实验正常完成,Strategy 默认参数保持不变。
248
+
249
+ `ltv_proxy@1` 的标准口径为:
250
+
251
+ ```text
252
+ LTV Proxy = DAU ARPU * (1 + D1 + D2 + D3 + D4 + D5)
253
+ ```
254
+
255
+ 其中 DAU ARPU 是实验窗口内总收入除以活跃用户天数;Dx 留存只使用截至评估时已经成熟的 cohort,并按 cohort 用户数加权。平台不会等待最后一天用户的 D5 成熟后再结束实验。
256
+
235
257
  所有会修改线上状态的实验命令都必须显式传 `--yes`。
236
258
 
237
259
  ## 脚本和配置
package/dist/gamealgo.js CHANGED
@@ -19,6 +19,16 @@ async function main() {
19
19
  printHelp();
20
20
  return;
21
21
  }
22
+ if (command === "version" || command === "--version" || command === "-V") {
23
+ const metadata = await loadPackageMetadata();
24
+ if (global.json) {
25
+ await printResult({ ok: true, name: metadata.name, version: metadata.version }, global);
26
+ }
27
+ else {
28
+ console.log(metadata.version);
29
+ }
30
+ return;
31
+ }
22
32
  try {
23
33
  if (command === "login") {
24
34
  await login(args, global);
@@ -353,7 +363,19 @@ async function handleExperimentStrategy(client, args, global) {
353
363
  await printResult(await client.archiveExperimentV2Strategy(strategyKey, sub === "archive"), global);
354
364
  return;
355
365
  }
356
- throw new Error("usage: gamealgo experiment strategy <show|publish|default|archive|restore>");
366
+ if (sub === "rollback") {
367
+ const flags = parseFlags(args);
368
+ const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
369
+ const versionId = optionalString(flags.version || flags["version-id"]);
370
+ const message = requiredMessage(flags, "experiment strategy rollback");
371
+ if (!strategyKey || !versionId) {
372
+ throw new Error("usage: gamealgo experiment strategy rollback <strategyKey> --version-id <versionId> --message <reason> --yes");
373
+ }
374
+ await requireYes(flags, global, "experiment strategy rollback");
375
+ await printResult(await client.rollbackExperimentV2Strategy(strategyKey, versionId, message), global);
376
+ return;
377
+ }
378
+ throw new Error("usage: gamealgo experiment strategy <show|publish|default|archive|restore|rollback>");
357
379
  }
358
380
  async function handleExperimentRun(client, args, global) {
359
381
  const sub = args.shift();
@@ -367,6 +389,12 @@ async function handleExperimentRun(client, args, global) {
367
389
  const body = await readObjectFile(filePath, "experiment run");
368
390
  if (!optionalString(body.displayName))
369
391
  throw new Error("experiment run requires displayName");
392
+ if (optionalString(body.objectiveTemplate) !== "ltv_proxy@1") {
393
+ throw new Error("experiment run requires objectiveTemplate: ltv_proxy@1");
394
+ }
395
+ if (!optionalString(body.baselineVariantId)) {
396
+ throw new Error("experiment run requires explicit baselineVariantId");
397
+ }
370
398
  if ((optionalString(body.type) || "manual") !== "managed" && body.startCondition !== undefined) {
371
399
  throw new Error("experiment run startCondition is supported only when type is managed");
372
400
  }
@@ -387,6 +415,33 @@ async function handleExperimentRun(client, args, global) {
387
415
  await printResult(response, global);
388
416
  return;
389
417
  }
418
+ if (sub === "status") {
419
+ const flags = parseFlags(args);
420
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
421
+ if (!runId)
422
+ throw new Error("usage: gamealgo experiment run status <runId>");
423
+ const response = await client.getExperimentV2Run(runId);
424
+ const run = response.run;
425
+ if (!run)
426
+ throw new Error(`experiment run not found: ${runId}`);
427
+ await printResult({
428
+ run: {
429
+ id: run.id,
430
+ strategyKey: run.strategyKey,
431
+ displayName: run.displayName,
432
+ type: run.type,
433
+ status: run.status,
434
+ objective: run.objective,
435
+ baselineVariantId: run.baselineVariantId,
436
+ requiredIntegrationVersion: run.requiredIntegrationVersion,
437
+ startedAt: run.startedAt,
438
+ endedAt: run.endedAt,
439
+ promotedVariantId: run.promotedVariantId,
440
+ currentAssessment: run.currentAssessment ?? null,
441
+ },
442
+ }, global);
443
+ return;
444
+ }
390
445
  if (sub === "evaluate") {
391
446
  const flags = parseFlags(args);
392
447
  const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
@@ -397,7 +452,6 @@ async function handleExperimentRun(client, args, global) {
397
452
  from: optionalString(flags.from || flags.start || flags.startDate),
398
453
  to: optionalString(flags.to || flags.end || flags.endDate),
399
454
  days: optionalString(flags.days),
400
- platform: optionalString(flags.platform),
401
455
  }), global);
402
456
  return;
403
457
  }
@@ -405,15 +459,29 @@ async function handleExperimentRun(client, args, global) {
405
459
  const flags = parseFlags(args);
406
460
  const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
407
461
  if (!runId)
408
- throw new Error("usage: gamealgo experiment run report <runId> [--report-id <id>]");
462
+ throw new Error("usage: gamealgo experiment run report <runId> [--round <number> | --report-id <id>]");
409
463
  const response = await client.getExperimentV2Run(runId);
410
464
  const reports = Array.isArray(response.run?.reports) ? response.run.reports : [];
411
465
  const reportId = optionalString(flags.report || flags["report-id"] || flags.id);
412
- const report = reportId
413
- ? reports.map((item) => objectValue(item)).find((item) => item?.id === reportId || item?.evaluationId === reportId)
414
- : objectValue(reports[reports.length - 1]);
415
- if (!report)
416
- throw new Error(reportId ? `experiment run report not found: ${reportId}` : "experiment run has no reports");
466
+ const round = flags.round === undefined ? undefined : positiveInteger(flags.round, "round");
467
+ const latestReport = objectValue(reports[reports.length - 1]);
468
+ const report = reports
469
+ .map((item) => objectValue(item))
470
+ .find((item) => {
471
+ if (!item)
472
+ return false;
473
+ if (reportId)
474
+ return item.id === reportId || item.evaluationId === reportId;
475
+ if (round !== undefined) {
476
+ const managed = objectValue(objectValue(item.report)?.managed);
477
+ return Number(managed?.roundNumber ?? Number(managed?.roundIndex) + 1) === round;
478
+ }
479
+ return item === latestReport;
480
+ });
481
+ if (!report) {
482
+ const selector = reportId ? `: ${reportId}` : round !== undefined ? ` for round ${round}` : "";
483
+ throw new Error(`experiment run report not found${selector}`);
484
+ }
417
485
  await printResult({ runId, report }, global);
418
486
  return;
419
487
  }
@@ -422,18 +490,20 @@ async function handleExperimentRun(client, args, global) {
422
490
  const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
423
491
  const variantId = optionalString(flags.variant || flags["variant-id"]) || optionalString(args.shift());
424
492
  if (!runId || !variantId)
425
- throw new Error("usage: gamealgo experiment run promote <runId> --variant <variantId> --yes");
493
+ throw new Error("usage: gamealgo experiment run promote <runId> --variant <variantId> --message <reason> --yes");
494
+ const message = requiredMessage(flags, "experiment run promote");
426
495
  await requireYes(flags, global, "experiment run promote");
427
- await printResult(await client.promoteExperimentV2Run(runId, variantId, optionalString(flags.message)), global);
496
+ await printResult(await client.promoteExperimentV2Run(runId, variantId, message), global);
428
497
  return;
429
498
  }
430
499
  if (sub === "cancel") {
431
500
  const flags = parseFlags(args);
432
501
  const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
433
502
  if (!runId)
434
- throw new Error("usage: gamealgo experiment run cancel <runId> --yes");
503
+ throw new Error("usage: gamealgo experiment run cancel <runId> --message <reason> --yes");
504
+ const message = requiredMessage(flags, "experiment run cancel");
435
505
  await requireYes(flags, global, "experiment run cancel");
436
- await printResult(await client.cancelExperimentV2Run(runId), global);
506
+ await printResult(await client.cancelExperimentV2Run(runId, message), global);
437
507
  return;
438
508
  }
439
509
  if (sub === "traffic") {
@@ -441,9 +511,10 @@ async function handleExperimentRun(client, args, global) {
441
511
  const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
442
512
  const variantId = optionalString(flags.variant || flags["variant-id"]) || optionalString(args.shift());
443
513
  if (!runId || !variantId)
444
- throw new Error("usage: gamealgo experiment run traffic <runId> <variantId> [--weight n] [--status running|paused] --yes");
514
+ throw new Error("usage: gamealgo experiment run traffic <runId> <variantId> [--weight n] [--status running|paused] --message <reason> --yes");
515
+ const message = requiredMessage(flags, "experiment run traffic");
445
516
  await requireYes(flags, global, "experiment run traffic");
446
- const body = {};
517
+ const body = { message };
447
518
  if (flags.weight !== undefined)
448
519
  body.weight = optionalNonNegativeInteger(flags.weight, "weight");
449
520
  if (flags.status !== undefined)
@@ -451,7 +522,13 @@ async function handleExperimentRun(client, args, global) {
451
522
  await printResult(await client.updateExperimentV2VariantTraffic(runId, variantId, body), global);
452
523
  return;
453
524
  }
454
- throw new Error("usage: gamealgo experiment run <create|show|evaluate|report|promote|cancel|traffic>");
525
+ throw new Error("usage: gamealgo experiment run <create|show|status|evaluate|report|promote|cancel|traffic>");
526
+ }
527
+ function requiredMessage(flags, action) {
528
+ const message = optionalString(flags.message);
529
+ if (!message)
530
+ throw new Error(`${action} requires --message <reason>`);
531
+ return message;
455
532
  }
456
533
  async function handleExperimentOverride(client, args, global) {
457
534
  const sub = args.shift();
@@ -957,6 +1034,12 @@ class GameAlgoAdminClient {
957
1034
  async archiveExperimentV2Strategy(strategyKey, archived) {
958
1035
  return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/archive`, { archived });
959
1036
  }
1037
+ async rollbackExperimentV2Strategy(strategyKey, versionId, message) {
1038
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/rollback`, {
1039
+ versionId,
1040
+ message,
1041
+ });
1042
+ }
960
1043
  async createExperimentV2Run(strategyKey, body) {
961
1044
  return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/runs`, body);
962
1045
  }
@@ -979,11 +1062,11 @@ class GameAlgoAdminClient {
979
1062
  async promoteExperimentV2Run(runId, variantId, message) {
980
1063
  return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/promote`, {
981
1064
  variantId,
982
- ...(message ? { message } : {}),
1065
+ message,
983
1066
  });
984
1067
  }
985
- async cancelExperimentV2Run(runId) {
986
- return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/cancel`, {});
1068
+ async cancelExperimentV2Run(runId, message) {
1069
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/cancel`, { message });
987
1070
  }
988
1071
  async listExperimentV2Overrides(runId) {
989
1072
  return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/overrides`);
@@ -1502,11 +1585,20 @@ function contentTypeForFileName(name) {
1502
1585
  return "text/javascript; charset=utf-8";
1503
1586
  return "text/plain; charset=utf-8";
1504
1587
  }
1588
+ async function loadPackageMetadata() {
1589
+ const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
1590
+ const name = typeof packageJson.name === "string" ? packageJson.name : "gamealgo-cli";
1591
+ if (typeof packageJson.version !== "string" || !packageJson.version) {
1592
+ throw new Error("CLI package version is missing");
1593
+ }
1594
+ return { name, version: packageJson.version };
1595
+ }
1505
1596
  function printHelp() {
1506
1597
  console.log(`
1507
1598
  GameAlgo CLI
1508
1599
 
1509
1600
  Usage:
1601
+ gamealgo --version
1510
1602
  gamealgo login --host <admin-url> --admin-key <game-admin-key>
1511
1603
  gamealgo whoami
1512
1604
 
@@ -1527,13 +1619,15 @@ Usage:
1527
1619
  gamealgo experiment strategy publish strategy.yaml --yes
1528
1620
  gamealgo experiment strategy default ad_frequency strategy.yaml --yes
1529
1621
  gamealgo experiment strategy archive ad_frequency --yes
1622
+ gamealgo experiment strategy rollback ad_frequency --version-id xv_xxxxxxxxxxxxxxxx --message "回滚参数" --yes
1530
1623
  gamealgo experiment run create ad_frequency run.yaml --yes
1531
1624
  gamealgo experiment run show xrun_xxxxxxxxxxxxxxxx
1625
+ gamealgo experiment run status xrun_xxxxxxxxxxxxxxxx
1532
1626
  gamealgo experiment run evaluate xrun_xxxxxxxxxxxxxxxx --from 2026-07-01 --to 2026-07-07 --yes
1533
- gamealgo experiment run report xrun_xxxxxxxxxxxxxxxx
1534
- gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant bravo --yes
1535
- gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --yes
1536
- gamealgo experiment run traffic xrun_xxxxxxxxxxxxxxxx bravo --weight 0 --status paused --yes
1627
+ gamealgo experiment run report xrun_xxxxxxxxxxxxxxxx --round 1
1628
+ gamealgo experiment run promote xrun_xxxxxxxxxxxxxxxx --variant bravo --message "采用胜出组" --yes
1629
+ gamealgo experiment run cancel xrun_xxxxxxxxxxxxxxxx --message "停止实验" --yes
1630
+ gamealgo experiment run traffic xrun_xxxxxxxxxxxxxxxx bravo --weight 0 --status paused --message "暂停异常组" --yes
1537
1631
  gamealgo experiment override set xrun_xxxxxxxxxxxxxxxx --user user-1 --variant bravo --yes
1538
1632
  gamealgo experiment override list xrun_xxxxxxxxxxxxxxxx
1539
1633
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gamealgo-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "GameAlgo command-line client for AI agents and game developers",
5
5
  "type": "module",
6
6
  "bin": {