@zhin.js/plugin-roulette 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/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # @zhin.js/plugin-roulette
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [db06de8]
8
+ - @zhin.js/core@1.1.48
9
+ - @zhin.js/game-kit@1.1.13
10
+
11
+ ## 0.1.1
12
+
13
+ ### Patch Changes
14
+
15
+ - e40899f: Adapters now project group owners and admins onto the canonical `sender.roles` names `owner` / `admin` (Telegram creator/administrator, KOOK guild owner and admin-permission roles, Lark chat owner/managers, QQ official guild owner/admin identities). KOOK numeric role ids moved from `roles` to `permissions` (`role:<id>`). `@zhin.js/command` exports `canManageConversation` ("private chat or owner/admin/master"), now shared by the Bilibili and earthquake subscription commands; Roulette lets owners and admins toggle the mute penalty and cancel a game.
16
+ - 84520a3: New game: Russian roulette (`轮盘` / `roulette`) for game-hub. Players join a lobby, take turns pulling the trigger on a cylinder with one bullet, and whoever is shot loses; turn and lobby timeouts are enforced by a schedule job, and results feed `战绩` / `排行 轮盘`. Groups can opt in to muting the loser; support is read from the Endpoint's declared `muteGroupMember` capability, so platforms that cannot mute only keep score.
17
+ - 37f79a1: 轮盘超时扫描:先读取禁言惩罚设置再提交状态流转,单个对局失败只记录日志并留待下次重试,结果在产生时即刻公布,不再丢失其他对局的通知或输家的惩罚。
18
+ - Updated dependencies [5be09ae]
19
+ - Updated dependencies [a3ab153]
20
+ - Updated dependencies [e8f7309]
21
+ - Updated dependencies [5afc18f]
22
+ - Updated dependencies [d3563d9]
23
+ - Updated dependencies [53d5bd7]
24
+ - Updated dependencies [c618eb7]
25
+ - Updated dependencies [fbfefb7]
26
+ - @zhin.js/core@1.1.47
27
+ - @zhin.js/game-kit@1.1.12
package/README.md CHANGED
@@ -25,10 +25,10 @@ database:
25
25
  | `轮盘 开始` | 房主开局,至少 2 人 |
26
26
  | `开枪`(或 `轮盘 开枪`) | 轮到你时扣动扳机;没轮到你时「开枪」只是普通聊天 |
27
27
  | `轮盘 退出` | 等待时离开;对局中离开算认输 |
28
- | `轮盘 取消` | 房主或机器人管理员取消本局,不计分 |
28
+ | `轮盘 取消` | 房主、群主、群管理员或机器人管理员取消本局,不计分 |
29
29
  | `轮盘 状态` | 查看当前对局 |
30
30
  | `轮盘 禁言` | 查看本群禁言惩罚和平台是否支持禁言 |
31
- | `轮盘 开启禁言` / `轮盘 关闭禁言` | 机器人主人(`master`)或受信任用户(`trusted`)切换本群禁言惩罚 |
31
+ | `轮盘 开启禁言` / `轮盘 关闭禁言` | 群主(`owner`)、群管理员(`admin`)、机器人主人(`master`)或受信任用户(`trusted`)切换本群禁言惩罚 |
32
32
 
33
33
  `/轮盘赌`、`roulette` 是同一组命令的别名;也可以从 `/游戏` 大厅进入。
34
34
 
@@ -43,7 +43,7 @@ database:
43
43
 
44
44
  ## 禁言惩罚
45
45
 
46
- - 按群开关,默认关闭。只有机器人主人或受信任用户可以切换,判断的是框架角色,不读取平台的群管理员身份。
46
+ - 按群开关,默认关闭。只有群主、群管理员、机器人主人或受信任用户可以切换;判断的是适配器投影到消息上的规范角色(`owner` / `admin` / `master` / `trusted`),不额外请求平台。
47
47
  - 插件不判断平台名称。每次需要时,用消息自带的 `conversation.endpoint`(适配器插件和 Endpoint 能力 id)走 `project(adapterFeatureId).connection(adapter, endpointId)`,再看 `resolveEndpointManagement(endpoint)?.muteGroupMember` 是否存在。存在就能禁言,不存在(或运行时没有 Adapter Feature)就只计分。
48
48
  - 超时判负由 `schedules/deadlines` 每 5 秒检查一次,它拿到的也是同样的能力上下文,所以超时的输家同样会按上面的规则处理。
49
49
  - 平台不支持禁言时无法开启;开启后平台失去这项能力,或禁言被拒绝(例如机器人不是群管理员),这一局照常结算,只是不禁言。
@@ -10,13 +10,11 @@ export const DEADLINE_CRON = '*/5 * * * * *';
10
10
  */
11
11
  export async function runDeadlineSweep(context, now) {
12
12
  const flow = resolveRouletteFlow(context);
13
- const notices = await expireOverdueSessions(flow, now);
14
13
  const outbound = readOutbound(context);
15
- if (!outbound)
16
- return;
17
- for (const notice of notices) {
18
- await announce(outbound, notice);
19
- }
14
+ await expireOverdueSessions(flow, now, (notice) => {
15
+ if (outbound)
16
+ return announce(outbound, notice);
17
+ });
20
18
  }
21
19
  function readOutbound(context) {
22
20
  try {
@@ -22,5 +22,5 @@ export interface ExpiredSessionNotice {
22
22
  }
23
23
  export declare function runRouletteAction(flow: RouletteFlow, actor: RouletteActor, action: RouletteCommandAction): Promise<string>;
24
24
  /** Called by the deadline schedule; each returned notice still has to be delivered. */
25
- export declare function expireOverdueSessions(flow: RouletteFlow, now: number): Promise<readonly ExpiredSessionNotice[]>;
25
+ export declare function expireOverdueSessions(flow: RouletteFlow, now: number, onNotice?: (notice: ExpiredSessionNotice) => Promise<void> | void): Promise<readonly ExpiredSessionNotice[]>;
26
26
  export declare function conversationOfSession(session: RouletteSessionRow): PenaltyConversation;
package/lib/game-flow.js CHANGED
@@ -4,7 +4,7 @@ import { ROULETTE_HELP } from './roulette-actions.js';
4
4
  import { playerFromMessage, stateOf, } from './session-service.js';
5
5
  import { describeAbort, describeClick, describeEnding, describeLobby, describeRoundStart, describeStatus, } from './view.js';
6
6
  const DEADLINE_ACTOR = 'deadline';
7
- const MANAGER_ROLES = ['master', 'trusted'];
7
+ const MANAGER_ROLES = ['master', 'trusted', 'owner', 'admin'];
8
8
  const NO_GAME_TEXT = '本群没有进行中的轮盘,发送「轮盘 开局」创建一局。';
9
9
  export async function runRouletteAction(flow, actor, action) {
10
10
  const message = actor.message;
@@ -27,13 +27,25 @@ export async function runRouletteAction(flow, actor, action) {
27
27
  return [...notices, reply].join('\n\n');
28
28
  }
29
29
  /** Called by the deadline schedule; each returned notice still has to be delivered. */
30
- export async function expireOverdueSessions(flow, now) {
30
+ export async function expireOverdueSessions(flow, now, onNotice) {
31
31
  const overdue = await flow.services.sessions.listOverdue(now);
32
32
  const notices = [];
33
33
  for (const session of overdue) {
34
- const text = await expireSession(flow, session);
35
- if (text)
36
- notices.push({ session, text });
34
+ try {
35
+ const text = await expireSession(flow, session);
36
+ if (!text)
37
+ continue;
38
+ const notice = { session, text };
39
+ notices.push(notice);
40
+ // Announce as soon as the result is stored, so a later failure cannot drop it.
41
+ if (onNotice)
42
+ await onNotice(notice);
43
+ }
44
+ catch (error) {
45
+ // The session stays active when anything before the commit failed; the next sweep retries it.
46
+ const reason = error instanceof Error ? error.message : String(error);
47
+ console.warn(`[roulette] 处理超时对局 ${session.id} 失败,下次扫描重试:${reason}`);
48
+ }
37
49
  }
38
50
  return notices;
39
51
  }
@@ -46,7 +58,7 @@ async function handleGameAction(flow, actor, action, session) {
46
58
  if (action === 'status')
47
59
  return describeStatus(stateOf(session), flow.config);
48
60
  if (action === 'cancel' && !mayCancel(session, message, actor.roles)) {
49
- return '只有开局的人或机器人管理员可以取消本局。';
61
+ return '只有开局的人、群主、群管理员或机器人管理员可以取消本局。';
50
62
  }
51
63
  const gameAction = toGameAction(action, message);
52
64
  if (!gameAction)
@@ -98,12 +110,17 @@ async function describeResult(flow, action, result, message) {
98
110
  return describeLobby(state, flow.config);
99
111
  }
100
112
  async function expireSession(flow, session) {
113
+ // Read everything that can fail before the state transition is committed: once the
114
+ // session is stored as finished it is no longer overdue and would never be retried.
115
+ const penaltyEnabled = stateOf(session).phase === 'playing'
116
+ ? await flow.services.settings.isMutePenaltyEnabled(session.channel_key)
117
+ : false;
101
118
  try {
102
119
  const result = await flow.services.sessions.applyAction(session, DEADLINE_ACTOR, { type: 'expire' }, flow.config);
103
120
  if (!result.applied)
104
121
  return '';
105
122
  if (result.state.phase === 'finished')
106
- return finishText(flow, result.session, result.state);
123
+ return finishText(flow, result.session, result.state, penaltyEnabled);
107
124
  return describeAbort(result.state);
108
125
  }
109
126
  catch (error) {
@@ -113,16 +130,17 @@ async function expireSession(flow, session) {
113
130
  throw error;
114
131
  }
115
132
  }
116
- async function finishText(flow, session, state) {
117
- const penalty = await applyLoserPenalty(flow, session, state);
133
+ async function finishText(flow, session, state, penaltyEnabled) {
134
+ const penalty = await applyLoserPenalty(flow, session, state, penaltyEnabled);
118
135
  return [describeEnding(state), penalty].filter(Boolean).join('\n');
119
136
  }
120
137
  /** Mute the loser only when this group opted in and the Endpoint declares muting. */
121
- async function applyLoserPenalty(flow, session, state) {
138
+ async function applyLoserPenalty(flow, session, state, penaltyEnabled) {
122
139
  const loser = loserOf(state);
123
140
  if (!loser)
124
141
  return '';
125
- const enabled = await flow.services.settings.isMutePenaltyEnabled(session.channel_key);
142
+ const enabled = penaltyEnabled
143
+ ?? await flow.services.settings.isMutePenaltyEnabled(session.channel_key);
126
144
  if (!enabled)
127
145
  return '';
128
146
  const conversation = conversationOfSession(session);
@@ -149,7 +167,7 @@ async function handlePenaltySetting(flow, actor, action) {
149
167
  return describePenaltyStatus(enabled, support === 'supported', flow.config);
150
168
  }
151
169
  if (!isManager(actor.roles))
152
- return '只有机器人主人或受信任用户可以修改禁言惩罚。';
170
+ return '只有群主、群管理员、机器人主人或受信任用户可以修改禁言惩罚。';
153
171
  if (action === 'penalty-off') {
154
172
  await settings.setMutePenalty(key, false, message.$sender.id);
155
173
  return '已关闭禁言惩罚,本群轮盘只计分。';
@@ -42,7 +42,7 @@ export const ROULETTE_HELP = [
42
42
  '轮盘 开始 — 房主开局(弹巢随机装 1 发子弹)',
43
43
  '开枪 / 轮盘 开枪 — 轮到你时扣动扳机',
44
44
  '轮盘 退出 — 等待时离开;对局中离开算认输',
45
- '轮盘 取消 — 房主或机器人管理员取消本局(不计分)',
45
+ '轮盘 取消 — 房主、群主、群管理员或机器人管理员取消本局(不计分)',
46
46
  '轮盘 状态 — 查看当前对局',
47
47
  '轮盘 禁言 / 开启禁言 / 关闭禁言 — 查看或切换本群的禁言惩罚(默认关闭)',
48
48
  '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-roulette",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Zhin.js 俄罗斯轮盘 — 多人轮流开枪,可按群开启禁言惩罚 (Plugin Runtime)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -30,15 +30,15 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@zhin.js/core": "1.1.46",
34
- "@zhin.js/game-kit": "1.1.11"
33
+ "@zhin.js/core": "1.1.48",
34
+ "@zhin.js/game-kit": "1.1.13"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^6.0.3",
38
38
  "vitest": "^4.1.10",
39
- "@zhin.js/schedule-feature": "1.1.6",
40
- "zhin.js": "1.1.11",
41
- "@zhin.js/testing": "1.0.4"
39
+ "@zhin.js/schedule-feature": "1.1.7",
40
+ "@zhin.js/testing": "1.0.5",
41
+ "zhin.js": "1.1.13"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
@@ -79,10 +79,10 @@
79
79
  "plugins": []
80
80
  },
81
81
  "peerDependencies": {
82
- "@zhin.js/middleware": "^1.1.7",
83
- "zhin.js": "^1.1.11",
84
- "@zhin.js/command": "^1.1.8",
85
- "@zhin.js/schedule-feature": "^1.1.6"
82
+ "@zhin.js/command": "^1.1.9",
83
+ "@zhin.js/middleware": "^1.1.8",
84
+ "zhin.js": "^1.1.13",
85
+ "@zhin.js/schedule-feature": "^1.1.7"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@zhin.js/command": {