dingchenhui0618-arch/dsh-doudizhu ↗★ 0
dsh-doudizhu
Three-player LAN Doudizhu (斗地主) for DeepSeek Harness: one authority host plus two followers, each playing from the plugin's floating window, with four levels of computer player filling any empty seat. 适合在DSH内进行局域网联机或与不同强度AI对战斗地主的用户。
Other repositories with this package name
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:dingchenhui0618-arch/dsh-doudizhuREADME
Read the full README ↗dsh-doudizhu
DeepSeek Harness 的插件:三人局域网斗地主。三个人各自在自己那台 DSH 里用浮窗打牌, 其中一台当权威服务,另外两台连过来。人不够也能玩:空位可以交给电脑补,四档强度 (简单 / 中等 / 困难 / 地狱)随时可换,一个人或两个人照样能打完整一局。
当前进度:规则引擎、牌局状态机、权威房间服务与传输、DSH 插件两个半区(host + 浮窗/标签页 客户端)、电脑玩家都已完成并有测试覆盖(144 个用例);已开源推送(仓库 public、v0.1.3 已发布、 市场 PR 的两项 check 全绿)。只剩三机实机验收没跑——那需要另外两台真机器。 具体的完成边界见「进度」一节,不要按 README 的愿景当成已实现的能力。
规则口径
一副 54 张牌(52 张 + 大小王),花色不分大小。 大小顺序:大王 > 小王 > 2 > A > K > Q > J > 10 > 9 > 8 > 7 > 6 > 5 > 4 > 3。
| 环节 | 口径 |
|---|---|
| 发牌 | 随机洗牌,三家各 17 张,留 3 张底牌 |
| 叫分 | 从首叫位起顺时针,依次叫 1 / 2 / 3 分或「不叫」;必须比当前最高分更高 |
| 定地主 | 叫到 3 分立刻成地主;否则三家各叫一次,最高分者当地主并拿走 3 张底牌(20 张) |
| 流局 | 三家都不叫则重新发牌,首叫位不变 |
| 轮换 | 每局结束后,首叫位顺时针轮换一位 |
| 出牌 | 地主先出,之后顺时针;同牌型同长度比点数;炸弹压普通牌型,王炸压一切 |
| 不出 | 只在跟牌时可以「不出」;先手必须出牌 |
| 收回台面 | 连续两家不出,台面清空,出牌权回到最后出牌的那家,可自由选牌型 |
| 胜负 | 地主先出完 → 地主赢;任一农民先出完 → 农民赢 |
你给的规则里没写、由本项目钉死的边界
这些都在 test/rules.test.mjs 里标了 PINNED,改行为前先改测试:
10JQKA合法;JQKA2、A2345、任何带 2 或王的顺子/连对/飞机非法。- 顺子 ≥5 张、连对 ≥3 对、飞机 ≥2 个连续三张。
- 飞机的核心三张必须来自正好三张的点数;手里四张同点不能拆出核心。
- 飞机的翅膀数量必须等于核心组数(三个单张,或三个对子)。
- 飞机带单时,翅膀里出现一对也算「两个单张」。
- 四带二(四张 + 两张单牌 / 两对)不是炸弹,压不了炸弹,也不能压普通牌型。
- 王炸最大且唯一,没有任何牌能压王炸。
- 两张王的 id 是
LJ/BJ:SJ是黑桃 J,复用它会让牌堆少一张(有回归测试)。
进度
| 阶段 | 状态 | 验收证据 |
|---|---|---|
M1 规则引擎 src/rules.js | ✅ 完成 | 26 个用例:18 个规则 + 8 个合法出法枚举(含穷举完整性:12 张手的 8 张以内所有子集都必须在枚举结果里) |
M2 牌局状态机 src/game.js | ✅ 完成 | 18 个用例,含固定种子整局重放 |
M3 权威服务 + 传输 src/room.js src/net.js | ✅ 完成 | 29 个用例:真 TCP/HTTP、长轮询、全程防泄牌断言、跨 OS 进程入座、局域网地址可达、权威自己驱动电脑出牌 |
M4 DSH 插件接线 src/host.js | ✅ 完成 | 15 个用例(含 3 个半区字段契约用例)+ scripts/live-check.mjs 在运行中的 DSH 里开房、经这台机器的局域网地址入座、打完一盘(60–70 步,含 36 次真的压牌) |
M5 浮窗 UI src/client.cjs | ✅ 完成 | 40 个用例:22 个展示逻辑 + 4 个浏览器半区依赖守卫 + 14 个跑真实 lib/client.js 的渲染/交互用例;真实 Chrome 走过开房→入座→叫分定地主;真 GUI 里两个槽位 active、标签页与浮窗药丸已抓图 |
M6 电脑玩家 src/bot.js | ✅ 完成 | 16 个用例:电脑视图字段集被钉死(多一个字段就红)、四档在全电脑/1人+2电脑/2人+1电脑三种桌上都能打完整局且一次都没提出非法出法、强度排序用同牌同座位的配对联赛量出来、地狱 的记账能力有构造用例。另有 scripts/live-bots.mjs 在重启后运行中的 DSH 里用「1 真人 + 2 台地狱电脑」打完一整盘:脚本只发 0 号位的 15 次请求,电脑自己出牌 22 次、叫分 2 次,零保底,54 张齐 |
| M7 三机实机验收 | ⬜ 未开始 | 步骤见下面「三机实机怎么跑」;本机的传输路径已通,缺的是另外两台真机器 |
| M8 开源推送 | ✅ 完成 | (public,v0.1.3 已发布);市场 PR #7526 的 Submission gate 与 check 全绿,等维护者合并 |
当前 node --test "test/*.test.mjs":144 个用例全部通过(约 8 秒)。
规则 18、出法枚举 8、牌局 18、房间 17、传输 12、插件 host 12、半区契约 3、展示逻辑 22、
客户端依赖 4、客户端渲染 14、电脑玩家 16。
还没验证的(别当成已经好了)
- 浮窗的展开态我没有抓到像素。 下面「在真 GUI 里看到的」有收起态药丸的抓图、 标签页的抓图,以及药丸文字随轮询变化的证据;展开态是使用者点开过的 (他能点中「—」把它收起,说明展开态确实渲染出来了),但我没能自己截到那一张。
- 电脑那一栏的按钮,我没在真 GUI 里用手指点过。 按钮存在与否、点了发什么请求,
是用
test/client-half.test.mjs驱动真实lib/client.js验的(4 个强度按钮、 补齐、撤下、局中禁用),不是肉眼看屏幕点的。 - 三机实机、防火墙、以及"另外两家真的在别的电脑上":一步都没跑,M7 的事。 目前 follower 路径是用 Node 客户端经本机局域网地址验证的,那是真实传输但不是真实浏览器。
- 深浅色主题下的对比度只在深色下看过。
让它出现在 GUI 里(实测:不用重启)
这一节我上一轮写错过,在这里纠正。 我原先说"dsh.client 元数据在启动时读一次,
所以必须重启 DSH 才会扫描到客户端半区",依据是探测 /plugins//client.js 返回 404。
那个探测不可诊断:已经正常工作的 dsh-holdem 同样 404(桌面版走的是 shell 自己的
carrier,不是那条 HTTP 路由)。结论是错的。
真实情况:把插件关掉再打开一次就够了,运行中的页面会通过 HMR 拿到新的 Loader entry,
并按 dsh.client 组合出客户端半区(官方文档 dsh-client-modules 的原话:
"Enabling an ordinary plugin adds its Loader entry … Re-enabling loads one instance with its styles")。
本机桌面版实测:
plugin_manager set_plugin include:dsh-doudizhu enabled=false -> application: applied
plugin_manager set_plugin include:dsh-doudizhu enabled=true -> application: applied
之后没有重启,客户端槽位树里立刻出现上面那两个 active 的占用者。
所以判断"客户端半区到底装上没有",要看客户端槽位树(cordis_inspect_query
→ client/Slots.listSubTree,root 填 shell.overlay 或 conversation.view),
不要看 HTTP 探测。
在真 GUI 里看到的
桌面版的窗口是 Electron,没有开远程调试端口,Kimi WebBridge 只能驱动 Chrome、够不到它;
未带会话的 GET / 返回 401,而认证是带在 URL 上的,所以新开标签页也进不去。
但抓它自己的窗口像素不需要输入通道:用 user32!PrintWindow 按窗口句柄截图即可
(只抓 DSH 这一个窗口,不截整个桌面)。看到的东西:
- 标签栏里出现了「斗地主」,位置在
审查与日报之间——正是注册的order: 30。 - 浮窗在浮窗层里渲染成收起态药丸:
● 斗地主 · 等人,有状态圆点、圆角背景、 文字是斗地主 ·。说明src/client-css.cjs的样式在真实宿主里生效, 没有被浮窗层的点击穿透规则吃掉。 - 药丸文字会自己变:两次抓图之间从
斗地主 · 等人变成斗地主 · 未进桌, 对应我这边把房间关掉。这是轮询与重渲染在真实宿主里跑着的直接证据 (收起态轮询间隔 2.6 秒,所以会看到一点延迟)。 - 交互在真实浮窗层里是通的:使用者点中药丸把它收起(
prefs.collapsed = true落盘), 拖动也生效。也就是说pointer-events: auto这个必需的重启用在真壳里有效。
抓图不随仓库发布:它拍的是整窗,含本机会话内容(对话记录、其他插件的名字)。
图留在被忽略的 _tmp/ 下,所以上面只有文字描述,没有图片链接。
真实浏览器里验过什么
scripts/browser-harness.mjs 是个桩宿主:它在真实 Chrome 里用真实 React UMD 加载真实的
lib/client.js,并把 /dsh-doudizhu/* 代理到真实运行中的插件。唯一 stub 掉的是 DSH 自己的
槽位外壳,其余全是真的。用它(经 Kimi WebBridge 驱动你的 Chrome)走完的过程:
- 两个槽位注册成功,浮窗与标签页同时渲染出大厅:
🃏 斗地主 · 未进桌、我开房/我加入、 昵称、端口、开房,标签页那份还有收起浮窗。 - 真点
开房→POST /dsh-doudizhu/host -> 200,房间码、局域网地址、 分享行、三人座位、事件日志我 入座全部渲染出来。 - 另外两家经局域网端口真的入座 → 自动发牌,界面显示三家各 17 张、
行动中、轮到你叫分、不叫/1/2/3 分四个按钮、叫分阶段徽标。 - 真点
3 分→ 变成地主、20 张、你先手,必须出牌、底牌K♥ 9♥ 3♠亮出、出牌/不出并排且显示先选牌;浮窗 651px 高,left:1566px; top:286px,整窗在屏幕内。
这一步抓到了两个真 bug,都是单测和 Node 渲染测不出来的:
prefsFrom把null当成了坐标 0。Number(null)是0而不是NaN,所以一旦{"x":null,"y":null}被写进localStorage,再刷新浮窗就跳到左上角。首次进入(存储为空,Number(undefined)才是 NaN)看着正常,所以只在第二次加载才暴露。- 浮窗按固定高度定位,20 张牌时底部被顶出屏幕。 窗口真实高度随内容变化(样式表上限 78vh),
而
clampPosition用的是常量MINI_H。现在先量真实盒子再夹取。
已验的安全性质
防泄牌不是靠约定,是靠断言。每个用例在每一手之后把三家收到的快照里所有字符串收集成集合, 再断言其中不含任何「此刻属于别人、且尚未公开」的牌面 id:
test/room.test.mjs—— 一盘完整对局的每一步都查一遍(含叫分、炸弹、王炸、过牌、胜负)。test/net.test.mjs—— 三个 follower 走真实 HTTP 打完一盘,每一步都查一遍, 并且要求每家的快照seq与权威一致(既查泄露,也查同步)。test/fixtures/follower-probe.mjs—— 独立 OS 进程入座后,otherHands必须是[null, null]。
公开的牌(打出去的牌、地主拿走后的 3 张底牌)不算泄露;一局结束后全明牌是刻意的复盘行为,
由 secretsRevealed 标记,且有测试钉住它只在结束后为真。
架构
机器 A(房主 / authority) 机器 B、C(客人 / follower)
┌──────────────────────┐ ┌──────────────────────┐
│ DSH + 本插件 │ │ DSH + 本插件 │
│ 浮窗 shell.overlay │ │ 浮窗 shell.overlay │
│ │ 回环 HTTP │ │ │ 回环 HTTP │
│ host 半区 authority │◄───────────┤ host 半区 follower │
│ 牌堆 / 判定 / 座位过滤 │ 长轮询 LAN │ 转发 + 本地快照 │
│ 0.0.0.0:
│ │ │
└──────────────────────┘ └──────────────────────┘
- 浮窗只跟自己那台机器的 host 半区说话:一份客户端代码,两种角色通用;浏览器不跨机直连。
- 传输用零依赖 HTTP 长轮询,不引入 WebSocket。回合制 + 不限时,延迟足够。
- 权威服务按座位过滤快照:每个人只拿自己的手牌,别人只给张数。
- 权威端口只在开房期间监听,散局即关。
host 半区的本地接口(M4 已实现)
浮窗调的是本机 DSH webServer 上的 prefix 路由,不是局域网端口:
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /dsh-doudizhu/state | 当前角色、房间码/地址、本座视角的快照;响应头带本地令牌 |
| POST | /dsh-doudizhu/host | 开房并坐 0 号位,返回房间码与所有局域网地址;可带 botLevel 指定补位电脑的强度 |
| POST | /dsh-doudizhu/join | 以客人身份连到某台权威(host + port + code) |
| POST | /dsh-doudizhu/probe | 只探一下某个地址是不是斗地主房间,不入座 |
| POST | /dsh-doudizhu/act | 转发一个动作(start / next / bid / play / pass / bots) |
| POST | /dsh-doudizhu/leave | 散局:关掉权威端口并回到 idle |
这些 POST 都要求 x-dsh-doudizhu-token(首次 GET 下发),并校验 Origin /
Referer / sec-fetch-site:跨站页面既设不了自定义头,也藏不住自己的来源。
dsh-app:// 这类非 http(s) 来源被当作「无信息」而不是拒绝,否则桌面壳会被误伤。
两次失败的尝试不会拆掉你已经在打的牌局:开房先绑定端口再关旧房, 加入先入座成功再切换角色。同名重进会复用原座位令牌,不会多占一个位子。
明确不做
不把 DSH 自己的 webServer 开到 0.0.0.0。 那个端口后面是完整的 agent API,
局域网里任何人都能驱动宿主的会话。游戏走独立端口,只开牌局端点。
一个实测到的边界
DSH 自己的 Web 认证不覆盖插件用 ctx.webServer.register 挂的 prefix 路由:
本机实测 /api/boot 未带会话时返回 401,而 /dsh-doudizhu/state 返回 200。
所以这个插件的三道校验(本地令牌、同源、sec-fetch-site)不是锦上添花,
而是改状态接口唯一的防线。响应不带 CORS 头,因此别的网页读不到响应体;
但要记住:任何本机进程都能读到 GET /dsh-doudizhu/state 里你自己的手牌。
这没有把攻击面扩大到局域网——路由挂在回环 webServer 上,权威端口另开且只承载牌局端点。 但它是一条值得记住的事实,将来 DSH 改了 webServer 认证覆盖面时应当重新核对。
本插件的 prefix 路由挂在 DSH 自己的回环 webServer 上,所以它天然不出局域网;
即便如此,所有改状态的操作仍要过上面那三道校验。
浮窗与标签页(M5 已实现)
客户端半区只注册两个入口,一份代码两种角色通用:
| 槽位 | id | 位置 |
|---|---|---|
shell.overlay | doudizhu-mini | 浮窗:可拖动、双击收起成悬浮药丸、位置与折叠状态存 localStorage |
conversation.view | doudizhu | 会话标签页「斗地主」:牌桌 + 记录并排,适合摆开看 |
浮窗里有「牌桌 / 记录」两个面:牌桌是座位、台面、手牌与操作按钮;
记录是权威写的中文事件日志(甲 入座、丙 入座,开局)加逐手出牌历史。
- 手牌按牌力从大到小排开,♥♦ 红、大小王单独着色;点一下选中抬起,再点取消。
- 「出牌」按钮选到合法牌型才可点,旁边直接写出你选的是什么(
顺子 · 5 张)或压不过上家的对子。这是本地即时反馈,权威仍然会自己再判一次。 - 「不出」按钮始终在位,靠置灰表达能不能点——按钮位置不随回合跳动。
- 浏览器半区只调本机回环路由
/dsh-doudizhu/*,从不直连权威端口, 所以三台机器跑的是同一份客户端代码,浏览器也不需要跨域访问对端。 - 样式全部走宿主主题令牌(
--dsw-alias-*),深浅色随宿主切换。
按 lib/client.js 的构建产物做单元测试:test/client-half.test.mjs 在一个干净的 V8
上下文里加载真实 bundle,配一个最小 React 与最小 DOM,驱动真实房间产生的快照,
断言槽位注册、渲染结果与按钮交互(选牌后「出牌」由灰变亮、再点又变灰)。
电脑玩家(人不够也能开局)
一个人、两个人也能打完整一局:空位交给电脑。电脑住在权威那台机器里,不在各自的 客户端里——三家的电脑由同一个进程算,牌桌状态只有一份真源。
怎么用
| 位置 | 操作 |
|---|---|
| 开房前(浮窗「我开房」页) | 「人机」一栏先选强度,开出来的房间就按这个强度补位 |
| 开房后(牌桌页「电脑」一栏) | 四个强度按钮 +「用电脑补齐空位」/「撤下电脑」 |
- 强度随时换:牌局进行中也能点,下一手立刻按新强度走。
- 数量只能局间改:局中加一个座位会让某个人没有手牌,所以这两个按钮在局中自动禁用。 这一栏只有房主看得到,客人改不了(改动由房间拒绝,理由写明)。
- 朋友中途来了:局间「加入」会顶掉编号最大的那个电脑座位,房主自己的位子不动;局中 「加入」被拒,理由是「牌局进行中不能换人」。
- 真人仍然没有回合计时,想多久想多久。电脑只是等 700ms 再动,好让人看清牌桌。
- 这两个按钮发出什么形状的请求有测试按在线上(
test/client-half.test.mjs: 补齐发{type:'bots', level, count:2}、点强度只换强度不动数量、局中数量按钮是灰的)。 「加入」当初就是死在两个半区的字段名对不上,新按钮不能只靠源码头读。
四档强度分别是什么
不是同一套算法加噪声,而是规则的严格包含关系:上一档的每条规则,下一档都有。
(src/bot.js 顶部那张表就是这份清单,代码与它对得上。)
| 规则 | 简单 | 中等 | 困难 | 地狱 |
|---|---|---|---|---|
| 能一把走完就走完 | – | ✓ | ✓ | ✓ |
| 压最小的牌、留炸弹 | – | ✓ | ✓ | ✓ |
| 农民不压快走完的队友 | – | ✓ | ✓ | ✓ |
| 有人快走完时不惜上炸弹拦住 | – | ✓ | ✓ | ✓ |
| 先手按「还剩几手」选牌 | – | – | ✓ | ✓ |
| 叫分更凶(更常当地主) | – | – | ✓ | ✓ |
| 记账:算得出「这手已经没人压得住」,拦不住就升级到炸弹;找得出必杀路线 | – | – | – | ✓ |
实测(node scripts/bot-league.mjs,同一副牌、同一座位,只换那一个座位的强度,每档 180 手):
| 难度 | 所在方胜率 | 对比中等自战 | 当地主时胜率 | 当上地主 |
|---|---|---|---|---|
| 简单 easy | 31.1% | −12.8pp | 55.9% | 19% |
| 中等 medium | 43.9% | —(对照组) | 76.9% | 36% |
| 困难 hard | 56.7% | +12.8pp | 86.7% | 42% |
| 地狱 hell | 57.8% | +13.9pp | 82.8% | 48% |
怎么读这张表:不要对着 50% 看——一个座位只占一边的一部分,而地主那边只有一个人。
真正的基线是「中等在自己桌上」那一行。简单 ↔ 中等 ↔ 困难 各差约 13 个百分点;
地狱与困难只差 1.1pp,这批样本里说不显著。地狱确实会在残局做出困难做不到的判断
(test/bot.test.mjs 里有构造用例:房东只剩一张牌时,困难出一张 7 去赌,地狱算出只有炸弹
拦得住),但统计上它俩基本同级。这里不把「地狱明显更强」当结论卖。
调参过程中被删掉的两条规则也记在这:早期给地狱加的「压得住就一直压」和「进残局就记账」 实测是 0 到 −3.3pp,也就是更弱。规则名字好听不代表有用,所以它们没了。
不作弊这件事是结构性的,不是承诺
电脑和权威在同一个进程里,物理上读得到任何一手牌,所以公平只能是「交给它的对象里没有」:
botView()是电脑唯一的入口,字段集被test/bot.test.mjs钉死(多一个字段测试就红): 只有自己的手牌、公开的出牌记录、以及各家的张数。- 它连牌堆顺序都拿不到。
outstanding是它自己用「整副牌 − 自己的手 − 已出的牌」算出来的, 那是任何会记牌的人都有的公开信息,不是偷看。 - 不会卡住:策略允许犯错,牌桌不允许停。每一手都过真引擎校验,另有两层保底出法; 真出错会写进房间日志而不是让三个人对着不动的牌桌。测试里这个计数必须是 0—— 四档、三种桌子(全电脑 / 1人+2电脑 / 2人+1电脑)、近 30 局,一次都没触发过保底。
- 权威的电脑由权威自己的定时器驱动(
BOT_DELAY_MS),不依赖任何客户端在线:test/bot.test.mjs里有一条用例走真实 HTTP,人只答自己的回合,其余全由权威自己走完。
装进自己的 DSH(另外两家要做的)
不用克隆、不用构建、不用在这台机器上装 Node。 装的是一份预构建的包,lib/ 已经在仓库里
(所以这个仓库的 .gitignore 故意不忽略 lib/)。把这一行地址交给 DSH 的插件管理器:
https://github.com/dingchenhui0618-arch/dsh-doudizhu/releases/download/v0.1.3/dsh-doudizhu-0.1.3.tgz
三条路都通向同一个动作,挑顺手的:
- 插件市场(本机装的是
dshmarket,界面里"点一下装好"):搜doudizhu或直接粘地址; - 设置 → 插件:粘贴上面那行地址安装;
- 让智能体装:
plugin_manager工具install_bundle,target 填那行地址。
装完关掉插件再打开一次(不是重启 DSH),浮窗和「斗地主」标签页就出现了, 原因见上面「让它出现在 GUI 里」。
v0.1.3 有个例外要说清:这一版改了宿主半区(电脑玩家住在权威那台机器里, 代码在
src/room.js/src/net.js)。切插件只能换客户端半区,换不掉宿主半区, 所以这一版要让 DSH 重启一次才能用上电脑玩家。只玩纯真人对局的话,切插件就够了。
这条路径我验到什么程度
- 验过:在一个干净目录里从上面这个 URL 装,5.7 秒装好;装出来的包能真的被加载出来,
name = "dsh-doudizhu"、inject = ["webServer"]、apply是函数,exports["./client"]、dsh.bundle.patch、dsh.client元数据齐全。 - 验过(先例):你这台机器的 profile 里
dsh-all-in就是用同样形态的 GitHub Release tarball URL 装的(profiles/desktop/package.json里能看见), 所以插件管理器接受这种 spec。 - 没验过:桌面版安装对话框的具体文案——那是个 Electron 窗口,我够不到, 没法替你点。如果你那台机器上按钮名字不一样,照意思找"从地址/URL 安装"即可。
从源码装(改代码时用)
link: 安装,改完 lib/ 立即生效,~/.dsh/profiles/ /package.json 里的依赖写成
本地目录的 link: 路径(本机就是这么装的):
"dsh-doudizhu": "link:"
再把 dsh-doudizhu 加进同一份 package.json 的 dsh.profile.bundles 数组:
git clone https://github.com/dingchenhui0618-arch/dsh-doudizhu
node scripts/build.mjs # 只有改了 src/ 才需要
三机实机怎么跑(M7)
三个人的机器都要先有 DSH。主机就是你现在这台;另外两家各做一次:
- 装 DSH,再按上面「装进自己的 DSH」把插件装上(一行 Release 地址,不用克隆不用构建);
- 关掉插件再打开一次,右下角出现浮窗、标签栏出现「斗地主」;
- 主机点「开房」,浮窗里会显示房间码和局域网地址,把
房间码 XXXX · 地址 :8791那一行发给另外两家; - 他们在自己的浮窗里填地址和房间码,点「加入」;
- 座位坐满就自动发牌,叫分定地主,顺时针出牌。第三个人不想等、或者人只来了一半, 主机点「用电脑补齐空位」即可——电脑占的座位和真人座位在同一张桌上,规则完全一样。
主机这台要放行入站 TCP 8791,Windows 默认拦入站。管理员 PowerShell:
netsh advfirewall firewall add rule name="dsh-doudizhu 8791" dir=in action=allow protocol=TCP localport=8791
不再需要时删掉:
netsh advfirewall firewall delete rule name="dsh-doudizhu 8791"
已经通到哪一步:scripts/live-check.mjs 在这台机器上对运行中的插件走完了整条路径——
真开房、真绑定局域网端口、另外两家作为 Node 客户端经真实局域网地址入座、打完一盘
(含 36 次真的压牌)、再开下一局、关房,全程做防泄牌扫描。但那不等于另外两家真的在
别的电脑上、真的用浏览器打——那才是 M7 剩下的事。
同事连不上怎么排查
先看这条:v0.1.1 及更早版本的「加入」按钮是坏的。 浮窗发的是 baseUrl,宿主读的是
url,字段名对不上,于是 加入 永远返回 请填写房主地址(例如 …)——地址填得再对也进不去,
座位从来没被占到。检查 能通(那条路由正好读 url),所以症状看着像"网络不通",实际是本机
两个半区之间的字段错位。v0.1.2 已修。判据:如果同事点 检查 得到"连上了"、点 加入 得到
请填写房主地址 而地址栏里明明有内容,就是这个问题。
装完新版只需要把插件关掉再打开一次,不用重启 DSH——客户端半区会经 HMR 换掉。实测这个
边界是分开的:切插件能换客户端半区,换不掉宿主半区(切完 /state 里缺的仍是新宿主
字段),宿主半区的改动要重启 DSH 才生效。这个 bug 的修复在客户端,所以切一次就够。
v0.1.3 是例外:电脑玩家住在宿主半区(权威那台机器里跑策略),所以它要重启一次 DSH 才出现。判据:重启后浮窗牌桌页会出现「电脑」那一栏(四个强度按钮 + 「用电脑补齐空位」); 看不到这一栏就是宿主半区还是旧的。纯三人真人对局不受影响。
其余按顺序排,别一次改三样:
-
地址是不是虚拟网卡的。 主机装了 WSL / Docker / 虚拟机 / VPN 时,
os.networkInterfaces()会把那些网卡的地址一起给出(例如172.22.0.1是 WSL 的vEthernet)。它们看着和局域网地址一样,别的电脑永远连不上。 本插件现在只把真实网卡的地址放进「发给另外两家」那一行,虚拟网卡的会在下面用一行 小字说明「已隐藏」,别手抄。本机实测:修之前给的是192.168.1.50 或 172.22.0.1(一半概率抄错),修之后只有192.168.1.50。 注意127.0.0.1指的是他自己那台机器,发给别人一定连不上——所以那一项标了「(本机)」。 -
房间是不是开着。 局域网端口只在开房期间监听:
role=idle时 8791 上什么都没有。 关房再开房之后房间码会变,要把新的那一行重新发一次。 -
防火墙。 本机入站是
BlockInbound默认策略,但已经有两条DeepSeek Harness的入站 Allow 规则(Protocol: TCP、LocalPort: Any、Public配置文件),而监听 8791 的 进程正是D:\.deepseek\DeepSeek Harness.exe,你的 WiFi 也正好是 Public——所以放行是够的。 换机器或换网络后如果不够,用上面 M7 那节的两条netsh命令。 -
网络本身。 公司 / 园区 WiFi 常开客户端隔离(同一个 SSID 下的机器互相看不见), 或者同事挂在另一个 SSID / VLAN 上。这一条在客户端改不了,只能这样确认:让同事直接用浏览器 打开
http://:8791/hello- 出 JSON(
{"ok":true,...})→ 网络通,问题在插件里填的地址或房间码; - 转圈超时 / 无法访问 → 网络或防火墙,先确认两个人连的是同一个 WiFi。
实际遇到的案子集中在第 1 条和第 2 条。
- 出 JSON(
开发
node --test "test/*.test.mjs" # 144 个单测:规则 / 出法枚举 / 牌局 / 房间 / 传输 / 插件 / 展示逻辑 / 电脑玩家 / 客户端
node scripts/build.mjs # esbuild 打包:src/host.js -> lib/index.js,src/client.cjs -> lib/client.js
node scripts/live-check.mjs # 对着运行中的 DSH 实机跑一盘(三真人,host 半区端到端)
node scripts/live-bots.mjs # 对着运行中的 DSH 实机跑一盘(1 真人 + 2 台电脑)
在真实浏览器里看浮窗(scripts/browser-harness.mjs):它需要一个本机运行的 DSH,先把两个
React UMD 构建放进 _tmp/,再把夹具起在回环端口上,然后用浏览器打开它。
curl.exe -s -o _tmp/react.js https://unpkg.com/react@18.3.1/umd/react.production.min.js
curl.exe -s -o _tmp/react-dom.js https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js
node scripts/browser-harness.mjs --port 8792 # 然后打开 http://127.0.0.1:8792/
夹具里的「开房」会真的在运行中的插件上建房,所以看完记得回插件里散局。
lib/ 是提交进 git 的构建产物:用户装的是预构建包,不该在自己机器上跑 esbuild。
改完 src/ 必须重新 build,否则跑的还是旧代码。
热更新边界:客户端半区(src/client.cjs)改完 build,桌面版若在跑 client-hmr
会推给浏览器;host 半区(src/host.js)改动要重启 DSH 才加载。
而给一个原本没有客户端半区的插件新加 dsh.client / exports["./client"],
不需要重启:关掉插件再打开一次就会重新扫描并组合,实测见上面「让它出现在 GUI 里」。
(我原先在这里写"必须重启",是错的。)
test/ 下除了 *.test.mjs 还有共享断言(test/helpers/leak.mjs)和子进程探针
(test/fixtures/follower-probe.mjs),所以 test 脚本显式带上 glob——
Node 默认会把 test/ 目录下所有 .mjs 当测试文件跑。
scripts/live-check.mjs 是本轮 M4 验收用过的实机脚本,也留给 M7 复用:
它从外部连上运行中的 DSH,开房、经局域网地址入座、打完一盘、再开下一局,最后关房。
--keep 可以留下房间给人看。
scripts/live-bots.mjs 是电脑补位的