STS2_taku-agent 的 CLI 不是一个简单的 curl 包装器。它的设计目标是把游戏运行时整理成适合 AI 使用的协议:
AI / developer -> ./sts CLI -> localhost:15527 HTTP API -> ObservationServer 主线程队列 -> GameSnapshotBuilder / ActionSurfaceBuilder / ActionExecutor -> STS2 运行时对象、Godot UI 节点、Harmony hooks这套分层的核心思想有三个:
context first:先识别当前状态,再决定查哪个窄接口。action surface first:执行前先读取合法动作和参数结构,写接口复用同一套 action name。wait and delta:动作后等待稳定状态,再读增量,避免重复消费完整状态。
因此 CLI 的价值不只是“命令更短”,而是把 AI 最容易犯错的地方封住:状态不稳定时不暴露动作、转场后自动等待、索引类动作只在当前快照内有效、失败时返回恢复建议和可关联日志。
CLI 入口在 tools/sts-cli/main.ts,仓库根目录的 ./sts 和 npm 包里的 bin/sts 都指向它。入口做四件事:
- 解析顶层 command 和 args。
- 创建 telemetry session。
- 用 telemetry 包装 HTTP client。
- 调用
commands/dispatch.ts。
dispatch.ts 是命令路由层。简单命令直接映射 HTTP endpoint,例如:
sts context -> GET /api/v1/contextsts actions -> GET /api/v1/actionssts combat hand -> GET /api/v1/combat/handsts get /api/v1/... -> GET 任意内部路径复杂命令会在 CLI 层聚合多个 endpoint:
sts next:并发读context和observation/compact。sts combat snapshot:并发读context、player/summary、actions、combat/summary、combat/actions、combat/hand、combat/enemies。sts room summary:读context、actions、必要的player/summary和当前 room 的窄接口。sts run snapshot:把 run、compact、room summary 和 combat snapshot 组织成默认规划视图。sts rewards claim-all-safe:循环领取非卡牌奖励,遇到卡牌选择停下。
这是一种有意的分层:底层 API 保持小而稳定,CLI 可以为常见工作流提供“刚好够用”的聚合视图。
sts exec 的参数合同很窄:
./sts exec ACTION [INDEX] [TARGET]./sts exec ACTION key=value key=valueCLI 会把它转成:
{ "actionType": "play_card", "parameters": { "index": 0, "target": "jaw_worm_0" }}然后请求:
POST /api/v1/actions/executeexec.ts 还维护了一组默认等待条件:
continue_game -> run_activeend_turn -> player_turnchoose_map_node -> room_readyproceed -> room_readyskip_card_reward -> room_readyopen_treasure -> treasure这让命令语义更接近“做完这个动作并等到下一步可判断”,而不是“发出一次 HTTP POST 就结束”。如果默认等待不合适,可以显式传 --wait-for、--wait-for-ready、--wait-for-room 或 --wait-for-run。
HTTP API 层
Section titled “HTTP API 层”HTTP server 在 mod 内部运行,监听 localhost:15527。ObservationServer 用 HttpListener 接请求,但所有需要读写 Godot/游戏对象的逻辑都会通过 RunOnMainThread 进入主线程队列。队列挂在 Godot SceneTree.ProcessFrame 上,每帧处理有限数量的 action,避免后台 HTTP 线程直接碰游戏对象。
读接口的统一流程是:
- 主线程构建
GameSnapshot。 - 从快照构建
ActionSurfaceSnapshot。 - 从快照构建
CurrentKnowledgeSnapshot。 - 更新 observation version。
- 根据 path 返回窄切片。
写接口 actions/execute 的流程更长:
- 解析
actionType和参数,支持action、action_type、actionType三种字段。 - 读取执行前快照和动作面。
- 在主线程调用
ActionExecutor.Execute(...)。 - 等待 post-action 快照,必要时等待稳定状态。
- 生成
delta、recovery、当前动作面。 - 写
action-execution.jsonl。 - 返回状态、correlation id、context、delta、恢复建议和下一步建议查询。
这就是为什么 API 返回的不是裸 success,而是一个便于 AI 继续推理的结果对象。
API 分层
Section titled “API 分层”当前 API 可以分成六层。
1. 能力和入口
Section titled “1. 能力和入口”/api/v1/capabilities/api/v1/context
capabilities 来自 ObservationApiCatalog,告诉调用方有哪些 endpoint、成本和适用 state。context 是默认第一跳,返回 stateType、roomType、overlayType、isStable、isTransitioning 和 recommendedQueries。
背后的运行时接入点主要是:
RunManager.Instance.IsInProgressRunManager.Instance.DebugOnlyGetState()LocalContext.GetMe(runState)runState.CurrentRoomNOverlayStack.Instance.Peek()NMapScreen.Instance.IsOpen
2. 低 token 观察层
Section titled “2. 低 token 观察层”/api/v1/observation/compact/api/v1/observation/delta/api/v1/run/api/v1/state/full
compact 是给 AI 的摘要,不追求完整;delta 比较前后快照,只返回变化事实;run 是长程规划需要的层级信息;state/full 只用于 debug。
这一层不依赖单独 hook,而是由 GameSnapshotBuilder 汇总运行时状态。战斗部分会受 patch 触发的 snapshot/export 体系辅助验证,但实时 API 仍以当前运行时对象为准。
3. 动作面
Section titled “3. 动作面”/api/v1/actions/api/v1/actions/execute
actions 由 ActionSurfaceBuilder 根据 context.stateType 分派:
menu -> continue_gamecombat -> play_card / use_potion / discard_potion / end_turn / selection actionsmap -> choose_map_nodeevent -> choose_event_option / advance_dialogueshop / fake_merchant -> shop_purchase / proceedrest_site -> choose_rest_option / proceedrewards -> claim_reward / proceedcard_reward -> select_card_reward / skip_card_rewardcard_select -> select_card / confirm_selection / cancel_selectiontreasure -> open_treasure / claim_treasure_relic / proceedactions/execute 由 ActionExecutor 执行同名动作。读写合同复用同一套 action name,是这个仓库最重要的 API 约束:AI 只要从 actions 里挑,就不应该自己发明动作。
4. 玩家和知识层
Section titled “4. 玩家和知识层”/api/v1/player/summary/api/v1/player/deck/api/v1/player/relics/api/v1/player/potions/api/v1/player/status/api/v1/knowledge/current/api/v1/knowledge/cards/api/v1/knowledge/relics/api/v1/knowledge/potions/api/v1/knowledge/status
玩家层负责动态构筑状态,知识层负责当前上下文里卡牌、遗物、药水、状态的静态文本缓存。这样 AI 可以先用 ID、计数和摘要做判断,只有需要解释效果时再查文本。
运行时来源主要是 Player、PlayerCombatState、deck/relic/potion/status model,以及 ObservationText 对本地化文本的安全读取。
5. 战斗层
Section titled “5. 战斗层”/api/v1/combat/summary/api/v1/combat/actions/api/v1/combat/hand/api/v1/combat/enemies/api/v1/combat/piles
战斗读接口主要来自:
CombatManager.Instance.IsInProgressCombatManager.Instance.DebugOnlyGetState()CombatManager.Instance.IsPlayPhaseCombatManager.Instance.PlayerActionsDisabledCombatRoomPlayerCombatState.HandCreature.CombatState- 敌人的
Monster.NextMove NPlayerHand里的 in-combat selection 状态
战斗 hook 目前有三个:
CombatManager.SetUpCombat:战斗开始时采样。CombatManager.SetupPlayerTurn:玩家回合准备完成后采样。Hook.AfterCardPlayed:卡牌结算后记录出牌和采样。
写操作里,play_card 最终使用 RunManager.Instance.ActionQueueSynchronizer.RequestEnqueue(new PlayCardAction(card, target));end_turn 使用 PlayerCmd.EndTurn(...);药水执行会按是否在战斗中选择 target 或走非战斗使用路径;弃药水调用 PotionCmd.Discard(...)。
这里最需要注意的是索引时效:play_card index=0 只对当前 hand/action surface 有意义。打出一张牌后,手牌顺序可能改变,所以 AI 必须重新读 actions 或 combat hand。当前 executor 已支持 expected_card_id、expected_title、expected_cost 这类 guard 来避免 stale index 误打。
6. 房间和 overlay 层
Section titled “6. 房间和 overlay 层”这些接口覆盖战斗外的实际 UI:
/api/v1/map/summary:NMapScreen、NMapPoint。/api/v1/event:NEventRoom、NEventOptionButton、NAncientEventLayout。/api/v1/fake-merchant:NFakeMerchant、NMerchantInventory、merchant/proceed/back controls。/api/v1/shop:MerchantRoom、NMerchantRoom、库存和购买控件。/api/v1/rest-site:RestSiteRoom、NRestSiteRoom、NRestSiteButton。/api/v1/rewards:NRewardsScreen、NRewardButton、NProceedButton。/api/v1/card-reward:NCardRewardSelectionScreen、NCardHolder、skip button。/api/v1/card-selection:NCardGridSelectionScreen、NChooseACardSelectionScreen、NCardGrid、confirm/cancel controls。/api/v1/bundle-selection:NChooseABundleSelectionScreen、NCardBundle。/api/v1/relic-selection:NChooseARelicSelection、NRelicBasicHolder。/api/v1/crystal-sphere:NCrystalSphereScreen、NCrystalSphereCell、工具按钮、proceed button。/api/v1/treasure:NTreasureRoom、Chest、NTreasureRoomRelicCollection、NTreasureRoomRelicHolder。/api/v1/overlay:未处理 overlay 或终局 overlay。
这些层面的“hook”多数不是 Harmony hook,而是 Godot UI 节点接入点。读接口通过节点树发现控件,写接口通过 ForceClick()、EmitSignal(...) 或游戏 command API 触发同一套 UI 逻辑。
wait.ts 是 CLI 里最有工程价值的一层。它不只是轮询 context.stateType,还会根据条件读取 combat summary、actions 或 overlay。
例如:
player_turn要求处于 combat,且combat.side === "player",并有 action。room_ready要求 context 稳定;战斗房要能读 combat summary,非战斗房要出现该 state 的 primary action。run_active要求离开 menu,稳定,且当前状态有可解释的动作面或战斗摘要。- 终局 overlay 会返回
status=terminal,而不是一直等到 timeout。
这避免了一个常见问题:只看 stateType=treasure 或 stateType=rewards 不代表 UI 已经能点。真正可行动的标志是稳定 context 加可用 primary action。
这套 CLI/API 不是为了暴露最完整的游戏状态,而是为了让 AI 少犯错。
所以它有几个明确取舍:
- 默认不读 full state;从
context开始,按需深入。 - 动作必须来自
actions;写接口不鼓励调用方自造参数。 - 转场中隐藏动作;宁可等待,也不要对半稳定 UI 执行。
- 日志面向 replay;每条 CLI 命令和每次 server action 都能按 correlation id 追踪。
- 聚合放 CLI 层;底层 endpoint 保持小接口,便于后续替换或加缓存。
如果要继续演进,下一步不是把 API 做大,而是让动作合同更稳定:例如给手牌里的每张卡暴露稳定 instance id,减少 index 在战斗中的脆弱性;再往上则可以做 replay timeline,把 CLI telemetry、action execution log 和战斗 action history 合成一条可读轨迹。