Repository navigation
v1.2.2
🐛 问题修复
- 推荐会给出牌桌上根本不允许的鸣牌:在线服务只用「吃 / 碰 / 杠」这样的类别名回应,程序便把手里能做的杠全部展开。他家打出 9p、手里三张 9p 又有四张 8p 时,卡片给出四行——跳过、碰 9p、大明杠 9p、暗杠 8p——而暗杠 8p 在那个时刻无法宣告。现在按决策点还原:鸣牌窗口只给这张牌的大明杠,暗杠与加杠只在自己摸牌后出现;手牌凑不出的鸣牌整行丢弃,不再退化成一张没有牌的杠
- 加杠那一行没有牌:加杠宣告在刚摸到的那张牌上,取牌时却只查了暗牌。台账现在把刚摸的牌算进取牌池
- 抢杠、国士无双抢暗杠、胡抜北的荣和记错了放铳方:雀魂的和牌信息不带放铳方,程序按「最后一张被打出的牌」归属——普通荣和没问题,但这三种和牌亮出的是杠或拔北,于是放铳方被记到上一张打牌的人头上,有时就是自己,引擎因此以为是我们放铳。现在追踪的是「最后亮出牌的人」(打牌、加杠/暗杠宣告、拔北),每局开始时清空
- 推理源徽标不跟着设置变:徽标只在建立连接或开局时算一次。中途开关 MJOT,或者保存、兑换、清除密钥之后,引擎已经在用在线模型作答,徽标却还写着本地。现在四条密钥写入路径与开关都会即时刷新
- MITM 模式下断线不提示:窗口被杀掉、页面刷新、网络掉线这类非正常结束不会触发
websocket-close,MITM 侧因此一直认为游戏还连着,「已断开」「已离开对局」的提示从不出现。连接生命周期现在也会在abort时结束 - 外部代理模式下游戏客户端仍在向阿里云 SLS 上报:内置窗口会在自己的会话里取消这类请求,走外部代理的游戏客户端没有 Electron 会话。现在代理会解密
*.log.aliyuncs.com,并用真实接口那样的空 200 在本地应答
🎨 UI/UX 增强
- 暗杠画成它本来的四张牌:此前沿用鸣牌画法(被鸣的牌 → 箭头 → 四张),暗杠因此多出一张牌和一个不存在的箭头。现在就是四张:两端翻面,中间两张正面朝上;含赤宝牌时赤五排在这两张的第一张(整组左数第二张)
- 提示不再自相矛盾:模型加载成功会撤掉先前那条失败提示,重连会撤掉「已断开」,加载成功也说明旧的「模型缺失」可以撤掉;同一次事件重复出现不再叠成一堆
- 悬浮窗也能显示提示:提示容器原先挂在仪表盘页面上,悬浮窗——玩家真正在看的那个窗口——即使产生了提示也没有容器渲染。容器上移到
App,两个窗口都会显示 - 提示停留时间缩短(5 秒 → 3 秒,3 秒 → 1.5 秒)
🚀 性能提升
- 他家吃、碰时不再向在线模型发一次注定没有结果的请求:引擎在那种时刻对本家没有合法动作,此前只看事件类型,没看是谁在鸣牌
⚡ 改进与优化
start_game改为标准 mjai 事件:去掉自造的id(本家座位)与is_3p,改为{ type, names, num_players },名字取自开局时服务器返回的昵称。引擎把start_game.id视为可选字符串,此前我们发的每条start_game都反序列化失败被静默丢弃——抓包日志里每局一条Engine ignored event: start_game- 本家座位改由桥接层随事件流传递,这也是 mjai 的做法(
/v3/react里player_id与events并列);引擎停止时也会忘掉上一局记下的座位 - 日志消息句式统一:陈述句以句号结束,以数值或「冒号接错误」结束的不加
🛠️ 维护与重构
- 渲染层的 IPC 调用全部收进
src/api/:desktop.ts负责窗口与应用命令,engine.ts负责状态快照,设置与在线相关的一并迁入;组件不再直接拼通道名,一侧改名不会只剩另一侧 - main 进程里反复手写的判断变成具名守卫:
isRecord、isPendingCheckout、parseField,以及attachablegetter 与requireText;Lua proto 解析器顺手去掉两个顶替校验的as断言 - 类型改为从声明它的模块导入,不再借用 electron 的全局命名空间(
Electron.BrowserWindow/Electron.Session)——正是它让干净安装下的tsc -p scripts直接失败 - 删掉取牌映射里两个永远走不到的分支:
toObject({ defaults: true })会填满每个声明字段
🧪 测试与质量
- 新增 6 个测试文件:杠的决策点、推理源徽标、购买状态恢复、提示投影、MITM 连接追踪、遥测信标识别;前端补了 6 个鸣牌牌序 / 暗杠牌序用例
- 全仓共 522 个单元测试、23 个集成测试、58 个 Rust 测试
- CI 的
typecheck在干净安装下不再失败
🐛 Bug Fixes
- A recommendation could offer a call the table never allows: the online service names a call only by its kind, so every kan the hand could ever declare was expanded. With three 9p in hand, a 9p discarded and four 8p, the card showed four rows — pass, pon 9p, daiminkan 9p, ankan 8p — where the ankan 8p could not be declared at that moment. The rows now follow the decision point: a claim window offers the open kan on the discarded tile and nothing else, closed and added kans appear only after your own draw, and a call the hand cannot pay for is dropped instead of degrading into a tile-less kan
- An added-kan row carried no tile: a kan is declared on the tile just drawn, but the ledger searched the concealed hand only. The pool includes that tile now
- Robbed tiles were charged to the wrong seat: Majsoul's win info names no discarder, so a ron was attributed to whoever discarded last — right for an ordinary ron, wrong when the tile came from a kan or a pulled north (chankan, kokushi on a closed kan, nukidora), where the win landed on the previous discarder, sometimes ourselves, and the engine believed we had dealt in. The tracker now remembers the seat that last revealed a tile — discard, kan declaration or nuki — and clears it when a round opens
- The inference badge lagged behind the settings: it was computed only when a connection opened or a game started. Toggle MJOT mid-session, or store, redeem or clear a key, and the engine already answered online while the badge still said local. All four key-writing paths and the switch refresh it now
- MITM mode never noticed a dead connection: a window killed, a page reloaded, a network dropped — none of these fire
websocket-close, so MITM kept reporting the game as connected and the "disconnected" and "left the game" notices never appeared. The connection now also ends onabort - Game client still reported to Aliyun SLS in external proxy mode: the built-in window cancels those requests in its own session, but game client routed through the external proxy has no Electron session. The proxy now decrypts
*.log.aliyuncs.comand answers a beacon locally with the empty 200 the real endpoint returns
🎨 UI/UX Enhancements
- A closed kan is drawn as the four tiles it is: it used to be drawn like an open call — claimed tile, arrow, then the tiles it takes — which put a fifth tile and a meaningless arrow on the card. It is four tiles now, the outer pair face-down, with the red five as the first of the two that stay visible (second from the left)
- Notices no longer contradict each other: a successful model load retires the standing failure toast, a reconnect retires "disconnected", and a load proves a stale "models missing" can go; a repeat of the same event no longer stacks up
- The HUD can show a notice: the toast container sat on the dashboard page, so the window the player is actually watching had nowhere to render one. It moved up to
App, and both windows show them - Toast durations are shorter (5 s → 3 s, 3 s → 1.5 s)
🚀 Performance
- Another seat's chi or pon no longer costs a request that cannot answer anything: the engine's legal set for our seat is empty there, yet the decision-point test looked at the event type without asking who called
⚡ Improvements
start_gameis a standard mjai event: our ownid(the seat) andis_3pare gone; it is{ type, names, num_players }now, with the nicknames the server returns as the game starts. riichienv typesstart_game.idas an optional string, so everystart_gamewe sent failed to deserialize and was silently dropped — one "Engine ignored event: start_game" per game in a capture- The seat travels beside the event stream, the way mjai keeps it (
player_idnext toeventsin/v3/react); the engine also forgets it on stop, so a decision after a shutdown is not attributed to the last table - Log messages read consistently: a statement ends with a period; one that ends with a value, or with the colon an error follows, does not
🛠️ Maintenance & Refactoring
- Every renderer IPC call is behind
src/api/now —desktop.tsfor window and app commands,engine.tsfor the status snapshot, with settings and online moved in beside them; components no longer spell a channel name, so a handler cannot be renamed on one side only - Conditions that kept being written by hand became named guards:
isRecord,isPendingCheckout,parseField, plus theattachablegetter andrequireText; the Lua proto parser also loses the twoascasts that stood in for checks - Types are imported from the module that declares them instead of borrowed from electron's ambient namespace (
Electron.BrowserWindow/Electron.Session), which is what made a clean-installtsc -p scriptsfail - The abortive-draw mapping loses two branches that could never run:
toObject({ defaults: true })fills every declared field
🧪 Testing & Quality
- Six new test files: decision points around kan, the inference badge, purchase-state restore, notice projection, MITM socket tracking and beacon classification; the frontend adds six cases for how a call's tiles are laid out
- 522 unit tests, 23 integration tests and 58 Rust tests in total
- CI's
typecheckno longer fails on a clean install
Full Changelog: v1.2.1...v1.2.2