Skip to main content
D

dsh-icey

danhuaxuanshu/dsh-icey

让 DeepSeek Harness 的 agent 真的能玩《艾希 ICEY》:实时画面、看得懂的截图、按得动的操作;战斗中插件持续主控、模型随时看画面微调。

Install

dsh plugin --profile web add github:danhuaxuanshu/dsh-icey

README

dsh-icey

让 DeepSeek Harness 的 agent 真的能玩《艾希 ICEY》——不是模拟、不是读存档,是看真画面、按真按键。

你直接跟它说「玩艾希」「往右走」「打它」,它调工具去做。没有面板、没有侧栏入口, 插件就是一组工具。

0.9.x 补了四样:icey_doctor 体检(四档徽章,回答「现在到底能不能跑」)、 连玩结局分五档(完成 / 部分完成 / 失败 / 中止 / 超时;Boss 血条一条没打空算「部分完成」)、 分段托管(pauseAtEnd:true,跑一段就暂停,并把暂停之前那张带完整读数的清晰帧交回来)、 以及修掉三个真雷:读不到 HUD 时盲按确认键(死亡菜单上 Enter = 再次作战)、 血条读数偏高(旧坐标系的框,冒出过 104%)、README 里 U 键的假描述。

0.7.0 把战斗改成了「插件持续主控 + 模型随时微调」:icey_play({ background: true }) 立刻返回, 插件自己一直打,模型可以边 icey_look 边用 focus / style 拧方向,stop: true 收手 —— 不再是「一次调用跑完才回来,中间模型全瞎」。同时补了空战连段、闪避加密, 和一条「打不过也不脱战」的规矩(死了复活重来即可)。

0.6.0 删掉了原来的主面板和侧栏入口。原因写在下面的专节里, 简单说:它的每一项对 AI 驱动场景都冗余,而它唯独有用的状态展示也看不到 —— 游戏占着前台,要看面板就得切窗口,切过去游戏就看不见了。


⚠️ 给维护的人:这个包在 profile 里是「链接安装」,别去搬那个 Junction

开发时插件用 link: 装进 profile:

profile/package.json   "dsh-icey": "link:D:/dsh/ICEY插件"
profile/node_modules/dsh-icey   →  Junction(目录符号链接)→  D:\dsh\ICEY插件

这就是「改本地文件立刻生效」的原因,但也带来一个已经真实踩过的坑:

🔴 不要用 Move-Item / 资源管理器「剪切」去搬走 node_modules\dsh-icey

Windows 的 Move-Item 对 Junction 会跟随重解析点,把目标目录的内容一起搬走。 实测后果:把 <profile>\node_modules\dsh-icey 搬到备份目录之后, D:\dsh\ICEY插件 里的源码(index.js / lib / probe / shots …)全被搬走了, 源码目录只剩一个空壳 —— 而 DSH 界面上看起来只是「插件没了」,非常容易误判成「代码丢了」。

正确停用方式(任选一种,都不会碰目标目录):

做法命令 / 位置
插件管理里移除设置 → 插件 → dsh-icey → 移除(pnpm 删除链接,不动目标)
装但不用cordis.patch.yml 加一行 - id: dsh-icey + disabled: true
只删链接本身cmd /c rmdir "<profile>\node_modules\dsh-icey"(不要加 /s)

万一已经搬走了:从备份目录把内容 Copy-Item 回 D:\dsh\ICEY插件(Copy 不跟随、可重复执行), 或者干脆从 npm 重装一份源码底稿:npm pack dsh-icey@0.1.0 解压即可(21 个文件,缺 probe/ shots/)。

其它注意点:

  • 别在插件目录里跑 pnpm install:会把 node_modules 建到源码目录里,跟 profile 的那份混在一起。要装依赖用 npm.cmd install <pkg> --no-save,或干脆不加运行时依赖(现在就是零依赖)。
  • npm publish 只发 files 白名单里的 21 个文件,probe/、shots/、workbuddy/、RELEASING.md 不会进包 —— 所以「本地有、npm 没有」是正常的。

只有一个插件条目(2026-10-22 改)

cordis.patch.yml 现在只插一行 dsh-icey。以前是两行(dsh-icey + dsh-icey/agent), 结果插件列表里出现两个标题完全相同的「艾希 ICEY」,很难分辨。

原因是宿主的规则:行的标题取 row.meta?.title ?? row.moduleName,而 metaOf() 是按包取 locale 的 meta.title —— 同一个包的两行,标题必然一样。

合并后:

原来现在
dsh-icey:路由 + 配置同一个入口,全部由 index.js 负责
dsh-icey-agent:11 个工具index.js 末尾 import agent.js 并调它的 apply()

优雅降级没有丢:不再靠 Loader 的 inject 决定装不装,而是在运行时用 ctx.get('tools') / ctx.get('attachments') 探测 —— 缺服务就只跳过工具,路由照常。

package.json 仍导出 "./agent": "./agent.js",留给「以后想把工具单独挂到某个 agent 预设」的场合。


它到底怎么做到的(以及为什么只能这么做)

三件事都是实测出来的,不是设计时的假设:

1. 按键:只有 SendInput 有用,而且必须在前台

ICEY 是 Unity 5.4 游戏,走 Raw Input / GetAsyncKeyState。它完全无视 WM_KEYDOWN —— 前台发、后台发都试过,画面一动不动。只有 SendInput 有效,而且只对前台窗口有效。

所以插件的「操作」= 抢前台 + SendInput。代价是会打断你正在做的事(切走你的窗口)。这个代价必须让你知道,所以:

  • allowFocus 关掉 ⇒ 插件只抓画面,一次都不抢焦点(但也就按不动)
  • icey_status 会如实写「现在能不能操作」和原因,不会假装成功

另外实测:ICEY 自己也会抢焦点(它会把前台抢回去)。所以插件不能假设「刚才抢到了现在还在」,每次动手前都重新确认一次。

2. 抓帧:PrintWindow + PW_RENDERFULLCONTENT

D3D11 窗口用普通 BitBlt 会抓到全黑。必须 PrintWindow(hwnd, dc, 2)。最小化时两种都抓不到(不过游戏大约 15 秒会自己把窗口还原)。

3. 抓帧和颜色转换都在 worker 线程里

一帧 1606×935 是 ~6MB。走子进程 stdin/stdout 就是每帧 6MB 序列化 —— 10fps 就是 60MB/s,不现实。所以后端跑在 worker_threads 里,帧数据用 ArrayBuffer 转移(零拷贝),BGRA→RGBA 和缩放也在 worker 里做完,主线程只转手。


给 agent 的 11 个工具

工具干什么
icey_start接上/启动游戏,返回能不能操作 + 首帧截图
icey_status窗口、pid、前台状态、动作队列、后端健康
icey_doctor体检:四档徽章(就绪/有风险/缺失/阻断)回答「现在到底能不能跑、差哪一样」。只读,不发任何一个键
icey_look看一眼现在画面(缩到 1280 宽省 token)
icey_screenshot高清截图(看小字、菜单高亮)
icey_act单个或一串动作(见下)
icey_play自己连着玩一段(advance / fight / explore),可后台跑 + 微调
icey_auto按一句话任务跑一段,达标就停(until:"safe" 脱战即停)
icey_playbook取规则卡(哪些门打不开、哪些键会退游戏、怎么判进展)
icey_stop松键 + 停止接管(不关游戏)
icey_dev开发自检:改完 lib/ 哪些没生效、能不能重载、跑回归

icey_act 能做什么

move / dash / attack / heavy / execute / jump / interact / pause / menu / yes / no / combo / sequence

sequence 是省时间的关键:单次 icey_act 要 6~12 秒(截图往返), 而一次 sequence 最多塞 12 个动作、只花一次往返:

icey_act({ sequence: ["jjji", "move:right:400", "dash:up:200", "wait:150"] })

combo 打取消衔接(柔化),键位:j轻 i重 l处决 u剑气(非索敌) o冲刺 k跳 w/s/a/d方向 q是 e否 8/9强化技 |分阶段。 例:jjjji=颤振、jjiii=磁阱、jjj|s+j=小迭影收刀。

失败一律返回 { ok: false, error, hint },绝不抛异常——抛了模型就看不到原因。

关于「动作生效了没」

icey_act 会抓前后两帧算差异,但它在返回里明说:ICEY 待机本身也有动画(实测 1.5 秒差 0.47%),所以「画面变了」不等于「你的按键生效了」。要确认角色真响应了,得看画面里角色的位置/朝向,或者连做两次同一动作对比。


招式表(从游戏程序集里挖出来的)

不靠攻略抄,直接扫 ICEY_Data\Managed\Assembly-CSharp.dll 里的字符串, 拿到游戏自己的内部动作名,再对回键位(PlayerPrefs 里那套):

招式键内部动作名 / 说明
地面连段JATTACK1..15 + SameComboNum 计数,最多 15 段
重攻击IHeavyAttack,破防/收尾
空中连段空格 → JAirAtk1..7 / AirCombo1..2
空中重击空格 → IAirAtkHv1..5
空中下劈空格 → S+IAirAtkDown
处决LExecute → QTE + 子弹时间 ExecuteTimeSlow
闪现 / 冲刺O(+方向)FlashUp/Down/Left/Right,二段 DoubleFlash / DoubleFlashAir
追击U(+方向)内部动作名 Chase。⚠️ U 不是索敌键(2026-10-26 实测):单按原地出剑气,不位移、不转身,不会朝敌人靠
湍流8BladeStorm(后期解锁)。消耗生命换免疫,性价比不如连段重击
流场9ShadeAtk(后期解锁)。同上

元游戏选项(不是战斗招式):旁白会突然问玩家问题,比如 「您是否更喜欢困难的道路?」—— 画面上一左一右两个选项,左边标 Q(是)、右边标 E(否)。 这两个键不是游戏里的动作,是对着玩家本人提问时的回答键。

2026-10-25 实测:按 Q 选「是」之后,画面黑屏转场 → ICEY 标题 → 主菜单 → 新周目。 所以它确实是一道真选择,会影响后续内容。

⚠️ dll 里的名字不都是艾希的招式。 原始字符串表(shots/moves-extracted.txt,766 行) 里混着敌人 AI 的状态和参数,这些玩家按不出来,别去找它们的键:

名字它其实是
CounterAttack敌人的反击 —— 旁边就是 counterAttackProbPercentage(按概率反手)和 canCounterAttack 判定
SpAttack敌人的特殊攻击 —— BeelzebubSpAttackSuccess = 别西卜的特殊攻击成功
DefenseAir状态机里的空中防御 —— FallToDefenseAir(从坠落转入)、JumperDefence(跳跳怪的防御)
HitArmor受击护甲 / 霸体,孤立出现,没有参数

它们对打 BOSS 反而更有用:counterAttackProbPercentage 可能是我们连段突然被打断的原因 (不是插件按错,是敌人按概率反手),willBeExecute / HitIntoWeakState / HitArmor 解释了「为什么打不动」。但这些只能当判据用,而画面里现在还读不出这些状态。

已经用上的(但这里有过一处更正):icey_play 打架分支里那个「chase」是 朝当前方向走一步 + 点一下 U,不是「朝敌人突进」—— 2026-10-26 实测 U 不索敌、不转身,位移全部来自按住的方向键(05763a9)。 真正拉近距离的是那一步位移,U 那一下只多一道剑气。 闪现(O)取消硬直是真的,这条没问题。

空战(0.7.0 起,0.9.1 补齐):上面表里早就有「空中连段 = 空格 → J」,但自动连玩一直没用上 —— 招式表里只有挑空(w+j)和斩落(s+j),中间那段空中输出是空的,等于空战只做了两头。

连段内容来源权重
k|jjjj|o空格跳 → 空中四连 → 闪取消收尾视频评论区5
k|jjj|o跳 → 空中三连 → 闪(收得更快)视频评论区4
w+j|jjj|o激发挑空 → 空中三连 → 闪(不斩落,保持高度)视频评论区4
k|ji跳 → J → I —— 冲澈,垂直下劈游戏内招式表2
k|jjii跳 → J → J → I → I —— 迭影(空),空中穿刺游戏内招式表2
k|jjji跳 → J → J → J → I —— 反吹,拍地弹起游戏内招式表2

后三条的键序直接抄自游戏内「招式升级界面」(2026-10-31 逐张截图核对), 并且当天实机验过形态:冲澈垂直下劈落地、迭影(空)拉出紫红色弧形穿刺、反吹拍地出一圈环形冲击波。

验证状态要分开说,这是两件事: ① 键序列和形态 — 已实机验证(当天手动按出来的,就是上面那句)。 ② 在自动托管里的表现 — 从未验证:三轮托管 combo 抽取 40 次,这三条一次没被抽中。 当时权重是 3/4/3(合计 10 / 总 68 = 14.71%),40 次全不中的概率 = (1-10/68)^40 = 0.17%。 ⚠️ 换成现在的权重(2/2/2,合计 6 / 总 64 = 9.38%),40 次不中概率是 1.95% —— 不再反常。 所以「40 次没抽中」这个观察只对旧权重成立,别拿它解读新权重。

⚠️ 已知缺陷:没有「敌人是否在空中」的检测。 实测反吹在周围没有敌人时照样会发出来 —— 白白送一次后摇。 人能看到空中有没有目标,插件看不到,所以这几条本质上是抽签。

决定(2026-10-31):暂不撤出自动池,权重从 3/4/3 压到 2/2/2 (跟 s+j / w+j / jo 同档)—— 偶尔出现、能攒到「自动跑起来什么样」的数据, 又不至于频繁空挥。注释里原来写「低权重」但实际给的是中高档,这次一并兑现。 攒够数据再决定是提权还是撤池。

第一条来源是 B 站《全站第一份艾希空战战法教程?》(BV1nj411q7BN)的评论区 —— 那个视频没有字幕、本地也抓不了帧,只能取到文字:「空中轻四后接闪类似于 jc, 可以取消动作后摇和衔接下一套」。


规则卡:icey_playbook

模型不知道 ICEY 的坑(哪些数字代表进展、哪扇门开不了、菜单里哪个键会退出游戏), 每次都靠试错,代价是几十秒真机时间和几次误操作(真的把游戏退掉过一次)。 所以把实测结论固化成了规则卡:

icey_playbook({})                  // 完整版(约 1000 字)
icey_playbook({ format: 'compact' })  // 4 行精华,省 token

也可以直接取:GET /api/dsh-icey/playbook(markdown),?format=compact 取精华。

规则卡的结构参考了 GameScaling 的系统提示写法 (Game Rules / Common Keys / Output Format / 说明),内容全部来自本机实测:

段内容举例
Game Rules左上角是血条+连击、右上角是钱;靠近可互动物会浮出白字提示框;有些门是剧情锁
Common KeysW/S/A/D 移动、J 攻击、I 重击、L 处决、空格跳、Enter 互动/确认、Esc 暂停
怎么判断真进展海拔/坐标数字变了、画面整体横移、$ 变大;原地小幅抖动 = 被挡住
危险动作菜单里的「退出」也是用 Enter 选的(实测把游戏退掉过)→ 盲按确认键每次连玩限 3 次

顺带那个项目里最有用的一条指令是:「局面没变就直接出动作,只在明显变化时才思考」。 这也是 icey_play 的设计原则 —— 它只在 events 里汇报值得注意的变化,不逐帧说话。


自主连玩:icey_play(一次调用玩一段)

前面 6 个工具都是「你做一步、我看一眼」。icey_play 是反过来的: 一次调用跑完一整段,中途不再问你,结束后汇报「做了什么、途中发生了什么、几张关键截图」。

icey_play({ seconds: 30, preset: 'advance', captureEvery: 12, maxFrames: 3 })
preset行为
advance(默认)往右推进 + 节奏性挥刀,卡住就按确认键
fight原地连打:重击/普攻交替,偶尔冲刺
explore走得更久、偶尔跳、少打架

分段托管:跑一段、停下来、把画面交回(0.9.0)

icey_play({ seconds: 15, preset: 'fight', pauseAtEnd: true })

跑完 15 秒就按 Esc 把游戏停住,下一次调用会自动从暂停菜单恢复。 真实节奏是「15 秒游戏时间 + 模型往返约 9 秒」,比一口气托管慢 60%,但游戏时间是冻住的,模型不急。

🔴 段末那一帧是在按 Esc 之前抓的,这一点不能反。 实测暂停画面下所有判据全部归零:hudBright / hp / shield / combo / promptBright / subtitle 都是 0.00%,bossBar 是 null —— 先暂停再抓,就只剩一张读不出任何数字的压暗图。 所以顺序是硬约束,自检里有一条源码断言盯着它。

后台跑 + 微调:战斗的默认玩法(0.7.0)

原来 icey_play 是阻塞的:一次调用跑完才返回。而工具调用是串行的 —— 它跑的时候模型发不出 icey_look,整场战斗对模型就是个黑箱 (实测一场 60 秒的战斗,模型只发了一次「开打」,中间全是盲的)。

把段切短压不掉这个盲区。真正的解法是让循环脱离「阻塞的工具调用」:

icey_play({ preset: 'fight', background: true, seconds: 90 })   // 立即返回,它自己一直打
icey_look()                                                     // 战斗中随时看,返回里带「⚔️ 后台战斗进行中…」
icey_play({ focus: 'left', style: 'cautious' })                 // 微调,约 0.7 秒生效,不重启
icey_play({ stop: true })                                       // 收手,下一步就停

实现上只有一处关键:工具层不 await 它。s.play() 返回的 promise 不 await, 循环照样在插件里跑,工具立即返回。循环里插三个检查点:

检查点每步做什么
微调参数policy.setOrder(this.bg.order) —— 把 focus / style 同步给策略
停手请求this.bg.abortRequested 为真就 break(约 1 秒内)
在场哨兵模型 90 秒没 act/look/play 就自己停手

可调的只有两个旋钮:

  • focus:auto / left / right。指定后压过黄箭头的自动索敌 —— 模型的判断优先于像素信号
  • style:auto / aggressive / steady / cautious。锁死战斗欲望档,不看血盾自动分档

没有「撤退」这个参数 —— 2026-10-26 用户原话「不要撤,血量低了就把攻击欲望降低」, 2026-10-27 又确认「一直打到底,死了复活重来」。血低只体现为 style 降档和闪得更勤。

后台跑的时候手工 icey_act 会被硬拒绝(不是提醒):两边同时 SendInput 会互相打断, 而且插进队列的动作会打乱策略自己的动作序列。要看画面用 icey_look, 要调战术用微调参数,要停手用 stop。

战斗结束(跑满 / 停手 / 哨兵)后结果留在 bgLast 里,icey_status 能看到 「上一场后台战斗:60.5 秒(fight),停因…」和完整的动作明细 —— 后台模式下模型拿不到同步返回值,停手原因只能从这里读。

它靠什么决策(没有视觉理解,这点必须说清)

三个信号 + 一条脱困阶梯,都很便宜:

信号怎么算拿来判断什么
hudBright左上 HUD 区域里亮像素的占比太低(≈0%)⇒ 看不到游戏画面 → 一下都不按,只等(2026-10-31 改,见下方)
motion相邻两帧的差异比例几乎不动 ⇒ 被对话框/菜单挡住
progress跟 2.5 秒前那帧比太低 ⇒ 原地踏步(被门/墙挡住)
hudKickHUD 区域亮度变化在跳 ⇒ 在打架 → 加大攻击频率

后两个是最难的部分,也是 2026-10-23 真机踩出来的:

只看相邻帧的 motion 会被待机动画骗过去。 标题界面有飘雪(motion 1.5–2%),策略以为"画面在动",一直走路挥刀却从不按 Enter, 永远进不了游戏;卡在大门前也一样,白按了 15 秒。 所以补了 hudBright(判断在不在游戏里)和 progress(判断有没有真进展)。

⚠️ 但「读不到 HUD ⇒ 按确认键推进」这条 2026-10-31 也作废了。 实测踩到:角色死了,画面是「再次作战 / 重新开始」的死亡菜单,hudBright 同样是 0, 跟黑屏过场在像素上分不出——那一下 Enter 选中的正是「再次作战」,闷头把进度丢掉。 同类雷还有强化台(Enter = 花钱升级)。

现在的行为:读不到 HUD 就只 wait,一下都不按,连续 9 次后停手把画面交回。 代价是游戏刚启动停在标题时插件不再自己按过去——少按一次只是慢几秒, 多按一次可能丢进度或花钱。哪种界面能按、哪种按了就出事,只有看画面的人知道。

卡住时的脱困阶梯(挨个试,不撞同一面墙):

退一步 → 按 Enter(互动/确认)→ 跳 → 重击 → (一轮都失败)→ 掉头走另一条路

掉头用 (阶梯长度 + 1) 取模:第 5 格才是掉头。之前用阶梯长度取模, stillStreak 是 4 的倍数时会被掉头抢掉,重击那一步永远轮不到。

判定连续失败用的计数是衰减不是清零(撞一下、相机抖一下、又停住的锯齿, 清零会让阶梯永远走不到「掉头」——真机实测过:40 秒里只按了 4 次确认键)。 连着 5 轮都没进展 ⇒ 主动停下,把画面交回,由模型/人判断这里到底是什么。

怎么分辨「这扇门打不开」

先说结论:插件不做像素级的门禁识别,因为实测不可靠。

试过的方法和结果(都有记录):

想法实测结果
靠「画面几乎不动」判断卡住❌ 标题界面有飘雪(motion 1.5–2%),卡在门前也有待机动画 → 一直以为在前进
靠「2.5 秒净进展」判断卡住✅ 有效(正常走路 >50%,卡住 ~1.5%)
靠「互动提示框」(「按 J: …」那个白字框)判断能不能开❌ 提示框白字实测亮度只有 ~180(阈值 200 以上全是 0%),而且位置一变就从 0.03% 掉到 0 —— 信号太弱太敏感,不用它做判据
提示框能不能靠像素检测✅ 能,但有三个前提:① 只在游戏窗口前台时渲染(焦点还给 DSH 就消失)② 必须原分辨率(缩到 400 宽细笔画就糊)③ 角色站定(攻击/走路时会消失)。实测:无提示 0.019%,有提示 0.23~0.40%
卡住时该按哪个键✅ 所有互动都是 Enter(PlayerPrefs 里叫 Search):开机关、对话确认、强化台。J 只是攻击键。我第一次把提示框里那个 Enter 键帽图标看成了字母 J,白折腾了两轮

参考项目 THUSI-Lab/GameScaling(米哈游 × 清华的 游戏智能体)的做法是把这件事交给训练过的 VLM:每帧看图,输出 200ms 的键鼠动作块。 我们这里没有那个模型权重,gm-autoplayer 的做法是 本地反射 + 模型当策略(hybrid)。所以这个插件按同样的分工来:

  • 插件(反射):发现没进展 → 按阶梯试一遍交互 → 还不行就判定这条路走不通,掉头
  • 模型(策略):读 icey_play 返回的摘要 + 截图,决定「这扇门是不是要等剧情/需要别的办法」

icey_play 返回里的 events 会写明:「第 N 秒 这处过不去(可能锁住的门/解谜)→ 掉头探索另一边」。 看到这条,就该自己看一眼截图再决定,而不是继续硬撞。

⚠️ 盲按确认键是真有风险的:ICEY 的暂停菜单里「退出」也是用确认键选的。 实测发生过一次——菜单开着时光标停在「退出」,策略按了 Enter,游戏被退掉了。 所以现在每次连玩最多盲按 3 次确认键(MAX_INTERACTS),超了就改用跳/重击。

真机实测(2026-10-23)

icey_play({seconds: 30}) → {move:24, attack:24, heavy:5},从「时计塔内」走到带标记的大门口
icey_play({seconds: 25}) → {move:28, attack:12, heavy:4, interact:1}
                           events: ["第 13s 卡住了,按了确认键"]
icey_play({seconds: 40}) → {move:33, attack:31, heavy:6, interact:2, jump:2}
                           events: ["第 18s 这处过不去(有提示框才能互动,没有提示的多半是剧情锁)→ 掉头探索另一边",
                                    "第 27s 这处过不去 → 掉头探索另一边"]
                           stopped: "玩满 40 秒"

掉头是真的发生了:卡在那扇打不开的门前,试完 J/Enter/跳/重击后翻转方向, 之后继续玩到 40 秒结束(没提前放弃)。

也就是说:能自己走路、打架、发现卡住并换招;那扇需要特定操作的门它开不了 —— 按设计它会判定「走不通」、掉头、并在摘要里说明,把判断权交回模型。

安全边界

  • 连玩期间只在开始时抢一次前台,结束时统一还原(不是每按一次键就把焦点丢回给你,那样窗口会来回闪)
  • 任何路径结束都会 releaseAll() 松键 —— 连崩溃/停止插件都不会留卡键
  • 同一个游戏同时只允许一个连玩任务
  • 中途截图默认 3 张(每张都花 token),maxFrames: 0 可以完全不要
  • 后台模式(0.7.0)另有三道闸:模型 90 秒没 act/look/play 就自动停手;单段上限非战斗 60 秒 / fight 120 秒; 停手后重开要隔 10 秒(那 10 秒只拦重开,不拦战斗中微调 —— 拦微调本来就是假约束)
  • 后台跑的时候手工 icey_act 直接拒绝,避免两边抢键

想验证它(不开游戏也能测)

node probe/play-loop-check.mjs     # 假后端跑完整循环:会推进、会卡住自停、不留卡键
node probe/autoplay-check.mjs 25   # 真机:接上已开着的 ICEY 玩 25 秒,存截图
node probe/hud-probe.mjs           # 量 HUD 特征(调阈值时用)

play-loop-check 把原生后端换成「合成帧 + 只记录按键」的假后端,真的调用 play(), 所以验的是我们自己的编排逻辑(循环、超时、松键、焦点还原、参数夹紧),不是游戏本身。 selfcheck.mjs 里另有 18 项针对信号与阶梯的纯逻辑测试。CI 里都会跑。


为什么没有面板

0.5.0 及之前,侧栏有一个「艾希」入口,打开是实时画面 + 几个按钮。0.6.0 删掉了。

删的理由不是"减法好看",是逐项核对下来它确实没用:

面板功能agent 这边的对应工具独立价值
接上游戏 / 只接窗口icey_start0
松开按键icey_stop0
自主连玩进度 37s/150sicey_play 的返回值0
报错信息工具直接返回0
实时画面游戏就在前台,不需要看缩水版0

五项没有一项是 AI 拿不到的。

而且它唯独有用的那件事(把状态摊在侧栏)也做不到:游戏占着前台, 要看面板就得切窗口,切过去游戏就看不见了 —— 只能二选一。改成"仪表盘"也一样, 还是同一个位置问题,除非分屏或双屏。

现在信息走对话:跑完一轮 agent 直接说「fight-start×7、处决 3 次、卡在 move×83」, 比仪表详细,还能追问。

保留了什么:设置页(设置 → 插件 → 艾希)还在,只读展示键位映射; Panel / IceyIcon 组件代码本身没删,注释里写了想恢复的话怎么放回来; 后端 /stream 路由也不动(没人调就不抓帧)。

面板原来用的推流协议(留作参考)

/stream 走 二进制帧(ICYF 头 + 原始 RGBA),不是 SSE、不是 base64: 浏览器用 fetch 的 body reader 累积字节直接 putImageData,服务端只 enqueue Buffer。 拉模式:浏览器不读就不抓帧。这套实现还在,只是现在没人消费它。


配置

「设置 → 插件 → 艾希」,全部字段 volatile,改完不用重启:

字段默认说明
launchModeattachattach 接已开的窗口(最稳)/ steam / direct
gameDirD:\SteamLibrary\steamapps\common\ICEY有 ICEY.exe 的那一层
exePath空直接指定 exe;留空用 gameDir\ICEY.exe
fps10/stream 推流帧率,1–15(0.6.0 起没有面板会调它,基本用不到)
captureModeprintwindowbitblt 只在前者失败时试
inputBackendsendinputpostmessage 实测无效,留作排查
allowFocustrue关掉 = 只看不动
maxFrameWidth1280给模型的截图宽度
windowedFixtruedirect 启动时加窗口化参数

键位

来自游戏自己的 PlayerPrefs(HKCU\Software\Shanghai FantaBlade Network Technology Co., Ltd.\ICEY),不是猜的:

W/S/A/D 移动 · J 攻击 · I 重击 · L 处决 · 空格 跳 · Enter 互动/确认 · Esc 暂停 · O 闪现 · U 剑气(非索敌,别当追击用) · 8/9 特殊技 · Q 对话选「是」 · E 对话选「否」

可以在配置的 keyMap 里改。


边界(明确不做的事)

  • 不读游戏内存、不注入 DLL、不改存档——只做「看窗口 + 发按键」,跟人坐在这儿玩一模一样
  • 不依赖 computer_* 工具——那套是给通用桌面操作用的,插件自己抓帧更快更准(而且不会误抓标题里带 ICEY 的 chrome 窗口)
  • 不关用户的游戏——icey_stop 只停止接管

自检

node selfcheck.mjs

45 项起步,现在 294 项:PNG 编码/解码往返、像素加工、控制映射、动作队列、Config 归一化、 场景归类、卡住检测、战斗三档与它的一整套遥测。不需要 DSH、不需要游戏在跑。


已知问题与修复记录

下面这些记录里会提到「面板」—— 那都是 0.6.0 之前的事,面板已经删了。 记录保留是因为它们记的是当时真踩过的坑(抓帧为什么慢、为什么必须 PrintWindow、 为什么 attach 要自己试),这些结论跟面板在不在无关,仍然有效。

2026-10-21 修复:「开了艾希之后 DSH 不能用」

三个独立的真问题,都会让人感觉「插件把 DSH 搞坏了」:

  1. 抢前台后不还焦点 按键必须把游戏抢到前台(ICEY 只认前台输入),但旧版用完不还 —— DSH 就一直是后台窗口,点不动、打不了字。 现在每个动作都记着「动作前是谁在前台」,做完还回去(restoreFocus,默认开)。

  2. 抢前台时做了 TOPMOST → NOTOPMOST 抖动 如果后一步没成功,ICEY 就永久置顶,把 DSH 整个盖住。 这段已经删掉:宁可少抢到一次前台(会如实报错),也绝不留下置顶状态。 实测抢完 / 还完后 ICEY 的扩展样式里没有 WS_EX_TOPMOST 残留。

  3. 实时画面用 SSE + base64 推原始分辨率 一帧 1606×935 的 RGBA 是 6 MB,base64 后 8 MB。实测光在宿主主线程上编码 + 拼串就 147 ms/秒(10fps),浏览器那边 atob + 逐字节复制还要 41 ms/帧 ⇒ 整个 Web GUI 卡死。 现在改成:960 宽(2.15 MB/帧)+ 二进制帧(不 base64)+ 拉模式 (没人拉就不抓帧,面板一关 CPU 立刻归零)。浏览器侧 0 次 atob、0 次逐字节循环。

2026-10-21 修复(第二轮):面板按钮看不见、点了没反应

  1. 主按钮白底白字 —— 这个主题里 --dsw-alias-brand-primary 是浅色,按钮又写了 color:#fff, 于是文字和背景一个色 ⇒ 按钮是"空"的。改成 color: var(--dsw-alias-bg-base),深浅主题都对比得住。
  2. 点「接上游戏」没反应 —— 旧逻辑只做 attach;游戏没开时服务端会静默等窗口 60 秒, 用户看到的就是"点了没反应、游戏打不开"。现在:
    • /launch 在 attach 失败时立刻返回(<200 ms)并说清原因
    • 面板的主按钮是"接上,接不上就启动":先 attach,没接上就 direct 启动并提示在等什么
  3. 接得太早 —— Unity 会先开一个 320×240 的启动窗,几秒后才长成正常分辨率。 以前"窗口一存在就接",抓到的是启动窗。现在要求宽度 ≥ 640 才接。 冷启动实测:2.3 秒从"游戏没开"到"接上 1606×935"。
  4. 句柄失效后不会自愈 —— direct 启动时 Unity 会再 fork 一次, 我们最初拿到的 pid/hwnd 随第一个进程退出而失效,游戏却还在(新 pid + 新 hwnd)。 旧版会一直报"游戏已退出",可屏幕上游戏明明开着。现在 status / look / act 都会先自愈重接。
  5. 面板一打开会自己 attach 一次(不启动游戏),已经在跑的游戏不用点任何按钮就能看到画面。

icey_act schema 无效

dsh-tools 拿不到时用的是插件自带的等价实现,早期版本把参数原样交给宿主 —— 但 { action: { required: true } } 是 dsh-tools 的私有写法,不是合法 JSON Schema (合法写法是对象级 required: ["action"])。这个错只在模型调工具时才报,安装阶段看不出来。 现在兜底实现会做 spec → JSON Schema 转换,并且 probe/schema-check.mjs 会把 6 个工具 的 schema 逐个当合法 JSON Schema 校验一遍。

2026-10-21 19:4x 复核(DSH 0.2.1-rc.2,WorkBuddy 报的那条):把宿主真正的校验器 (dsh-tools/json-schema)从 app.asar 切出来跑了一遍,见 probe/dsh-jsonschema.mjs 头部:

  • 6 个工具的 parameters / output.schema 全部通过;旧写法会被判红,报错原话 schema.properties.action.required is not supported on type "string" —— 与当初的报错一致, 说明校验器接对了(灵敏度自证在 probe/schema-check.mjs 里)。
  • 这台机器实测 dsh-tools 仍然 ERR_MODULE_NOT_FOUND ⇒ 走的是本地兜底, 所以兜底路径必须产出标准 JSON Schema。现在按「谁家的 defineTool」分流 (parametersForDefineTool(params, localFallback)):
    • 本地兜底 → toSchema() 标准 JSON Schema(对象级 required,属性里没有 required);
    • 宿主 defineTool → 原样 spec(宿主自己转)。以前不分流、一律转成 schema, 在「拿得到宿主 dsh-tools」的机器上会被当成参数表再包一层(参数全错)。
  • ⚠️ 改完必须重启 DSH(或让插件重载):ESM 模块缓存不跨进程刷新 —— 文件修了、旧进程还在跑旧模块,就会一直报同一句。这也是「明明改好了还报错」的常见原因。
  • 复核:node probe/schema-check.mjs(直接跑宿主校验器 + 三条路径断言)。

2026-10-26:战斗三档「到底会不会切」—— 用 593 张真机截图审出来

三档(healthy / cautious / danger)上线后,实机跑了好几轮都没在日志里看到过低档位文案。 光看那句「血量和护盾都还够」分不清三种可能:真的没掉血 / 检测被污染 / 基线被锚死。 所以这一步不是改策略,是先把观测做出来,然后拿存量截图一次审 593 帧。

新增 probe/tier-audit.mjs(只读截图,不碰游戏):

node probe/tier-audit.mjs shots/combat    # 593 帧真战斗
node probe/tier-audit.mjs shots/combat3   # 空地负样本

结论三条,都是量出来的:

  1. 三档逻辑本身是活的:593 帧里 healthy 167 / cautious 155 / danger 38,换档 8 次。 所以「没看到档位」不是逻辑没跑,是没被观测到 —— 档位文案原来只出现在部分动作的 why 里(combo 和续连那两条路径根本不带档位),而 why 每次决策都被覆盖、也不进事件。 现在改成:换档记一条 combat-tier 事件,每轮摘要末尾附一行档位统计。
  2. 基线确实被污染锚高(这是真 bug):基线取「历史最大值」,但 HP 框里混着剑光/爆闪的蓝, 593 帧里 hp 读数最大 86.39%、盾最大 16.19%,而满血/满盾参考值只有 68.97% / 10.12%。

⚠️ 上面那组数字(69% / 10%)已经作废。 它们是带窗口标题栏时代的坐标系, 后来抓帧裁掉了标题栏(画面 720 高),HP_BOX 却一直没跟着重算 —— 框上面多套了三行空白,把分母撑大,满血只算出 61.4%,还冒出过「104%」这种读数。 2026-10-31 用满血帧重新标定:HP_BOX.y0 0.097→0.104、HP_BASELINE_MAX 0.75→0.92、 FLOOR 0.62→0.84。同一张满血帧改前 61.41%、改后 92.86%。 护盾(SHIELD_BOX)还没重算,仍沿用旧框。 一帧污染就把分母永久抬高 ⇒ 真实剩 96% 血会被算成 77%,掉血幅度系统性高估。 修法:给基线加物理上限(HP_BASELINE_MAX = 0.75、SHIELD_BASELINE_MAX = 0.13, 在 lib/games/icey/layout.mjs)。修完基线 86.39% → 75.0%,同一帧的比例从 77% 纠正到 88%。 审计里还有 25 次读数顶到上限被夹住,这就是污染帧真的存在过的证据。

  1. icey_play dir=left 其实一直是生效的,坏的是文案:PRESETS.advance.desc 写死 「往右推进」,dir=left 时摘要照样说往右。上一轮因此误判成「参数没传进循环」,白查一轮。 现在 desc 不带方向,摘要按实际 dir 拼「往左/往右」,并且回归断言锁住(dir=left 时 摘要里不许出现「往右」)。

顺带记一条观察:593 帧里有 28 帧被判成「菜单」(红色文字行 ≥1)。战斗画面里的红色伤害数字 可能踩到这条判据 —— 目前只是记下来,没改阈值,因为还没确认那 28 帧到底是什么画面。

2026-10-26(续):菜单判定在默认分析宽度下本来是坏的

审三档时顺手发现的,比三档本身更严重 —— 暂停菜单在默认配置下根本认不出来。

findRedTextRows 靠「红色文字行」判菜单,但那条判据只在 1280×745 上标定过, 而插件实际喂进去的是 analysisWidth 宽的帧(默认 480)。实测(shots/menu-check 真菜单帧):

分析宽度判出的红色行
1280 / 9601 条 ✓
6400 条 ✗
480(默认)0 条 ✗
400 / 3200 条 ✗

原因很具体:缩到 480 后那行警告只剩 3 行高,被 minBandHeight = 5 整个滤掉。 后果:插件看不见菜单,也就不会按 Esc 退出它 —— 「停在主菜单里出不去」这类现象有它一份。

同时还有反方向的错:480 宽下战斗里的红色霓虹窗和红色伤害火花反而能过旧判据, 593 张战斗帧里 28 帧被判成「在菜单」,决策层因此按了 22 次 Esc(战斗中按 Esc 会把暂停菜单打开)。

修法(lib/ui/scan.mjs,两条都有实测依据):

  1. 高度门槛按分辨率折算:minBandHeight 以 1280 为基准,实际取 max(2, 5×width/1280)。
  2. 加「细笔画」判据:数红色的横向游程 —— 文字是一根根细笔画,特效是几块大色斑。 实测真菜单警告行平均游程 1.51px(480)/2.35px(1280),战斗红特效 3.05~32.76px。 归一到 480 宽后超过 2.4px 就当块状特效丢掉。

修完的红线回归(node probe/menu-audit.mjs,全用真实截图):

样本修前修后
真菜单 480/640/800/960/1280480、640 认不出全部 1 条 ✓
战斗 combat 593 帧判成菜单28 帧(22 次 Esc)0 帧
空地 combat3 / combat2 各 40 帧00
探索 watch 500 帧—2 帧(人工看过:都是真暂停菜单,w0013 就是「恢复游戏/退出」那一屏)

已知边界:分析宽度 320/400 下红色文字被缩得连颜色都不够了,仍判不出菜单。 默认 480 不受影响;真要把 analysisWidth 调到 400 以下,就得知道菜单处理会退化。

2026-10-27:能开暂停菜单但「完全不能移动」—— 是输入法,不是游戏

现象:icey_act 返回 ok、按键也真的发出去了,但角色一动不动;esc 和 enter 却都有效 (能开暂停菜单、能按确认)。看着像「游戏锁了移动」或者「SendInput 方向键失效」。

根因:中文输入法(0804)在中文模式下会把字母键和方向键截给候选框,只剩 esc/enter 能进游戏。 三个 SendInput 变体(扫描码 / +EXTENDEDKEY / 纯 wVK)全试过、全都「失败」—— 因为发送方式从来不是问题,键在半路就被输入法吃了。

判断方法(10 秒):打开暂停菜单按 ↓,看高亮动不动。不动 = 方向键压根没进游戏。

修法:接上游戏时把该窗口线程的键盘布局切到 en-US (LoadKeyboardLayoutW('00000409', KLF_ACTIVATE) → AttachThreadInput → ActivateKeyboardLayout → 再补一条 WM_INPUTLANGCHANGEREQUEST)。icey_start 返回里的 inputlang 就是切换结果, 窗口变了会重新切一次。

其余已知问题

  • 会抢焦点:这是 ICEY 的输入方式决定的(它完全无视 PostMessage)。不想被抢就关 allowFocus。
  • 推流大约 5–6 fps:不是写死限速,是抓帧本身就慢(PrintWindow 抓 D3D11 窗口 ~80–150 ms)。0.6.0 起没有面板消费它。 抓帧慢的时候会自动放宽间隔,绝不堆积。觉得占 CPU 就把 fps 调低,不影响画面质量。
  • 最小化抓不到画面:游戏大约 15 秒会自己还原,插件会如实报「已最小化」。
  • koffi 拿不到时只能看不能动:插件会降级并在 icey_status 里写明,不会静默假装能操作。
  • 这台机器上 DSH 设置页没有艾希的可配置字段:profile 树里没有 @deepseek-ai/schemastery, 所以 Config 是 undefined。作为补偿,插件自己提供一份能改的配置页 (设置 → 插件 → 艾希;侧栏那个入口 0.6.0 已删),改完写 $DSH_HOME/icey/config.json,立刻生效。

安全:这个仓库是公开的

仓库和 npm 包都是公开的,所以绝不能把密钥带进来。 2026 年 GitHub 上大面积出现硬编码 API key 泄漏(AI 服务的 key 同比 +81%), 最常见的原因就是 .env / config.json 被 git add -A 一起推了上去。

这个仓库为此做了三道防线:

  1. .gitignore 覆盖 .env*、*.pem、*.key、.npmrc、*.tgz、*.log
  2. probe/secret-scan.mjs:扫工作区 + git 历史里的密钥特征
    node probe/secret-scan.mjs            # 工作区
    node probe/secret-scan.mjs --history  # 连 git 历史一起
    node probe/secret-scan.mjs --all      # 含被 gitignore 的目录(本机自查)
    
  3. 两道自动闸门:npm publish 前会跑一次(prepublishOnly), GitHub Actions 发布前也会跑一次(扫历史)—— 命中就直接失败,不会发出去

记住那条铁律:key 一旦推到公开仓库,就当作已经泄露。 光删文件没用(历史里还在),必须去服务商后台作废并重新生成。

扫出来的东西如果真的推上去了,处理顺序是: 轮换密钥 → 用 git filter-repo 清历史 → 强制推送 → 通知所有协作者重新克隆。


排错用的探针

probe/ 下每个脚本都是能直接跑的,不需要 DSH:

node probe/secret-scan.mjs       # 密钥扫描(工作区 / --history / --all)
node probe/schema-check.mjs      # 6 个工具的 schema 是否合法 JSON Schema
node probe/client-check.mjs      # 客户端自注册 / slot / 二进制帧解析
node probe/tool-check.mjs        # 真调 6 个工具(抓帧 + 按键都是真的)
node probe/stream-route-check.mjs # 跑真 /stream 路由,量与帧率
node probe/config-check.mjs      # 配置持久化往返
node probe/tier-audit.mjs <shots目录>  # 战斗三档 + 血/盾读数审计(只读截图,不开游戏)

一键跑全部本地检查:npm.cmd run check

MIT

Related plugins