@zhin.js/plugin-word-riddle 1.0.0 → 1.0.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.
Files changed (71) hide show
  1. package/commands/riddle/[action:string=].ts +15 -0
  2. package/lib/engine.d.ts +0 -1
  3. package/lib/game-flow.d.ts +7 -8
  4. package/lib/game-flow.js +23 -16
  5. package/lib/game-flow.js.map +1 -1
  6. package/lib/index.d.ts +8 -2
  7. package/lib/index.js +8 -25
  8. package/lib/index.js.map +1 -1
  9. package/lib/memory-db.d.ts +6 -0
  10. package/lib/memory-db.js +18 -0
  11. package/lib/memory-db.js.map +1 -0
  12. package/lib/models.d.ts +5 -4
  13. package/lib/models.js +2 -2
  14. package/lib/models.js.map +1 -1
  15. package/lib/riddle-command.d.ts +4 -3
  16. package/lib/riddle-command.js +6 -2
  17. package/lib/riddle-command.js.map +1 -1
  18. package/lib/riddle-provider.d.ts +0 -1
  19. package/lib/riddle-provider.js +2 -1
  20. package/lib/riddle-provider.js.map +1 -1
  21. package/lib/riddles-catalog.d.ts +2 -0
  22. package/lib/riddles-catalog.js +2 -0
  23. package/lib/riddles-catalog.js.map +1 -0
  24. package/lib/runtime-store.d.ts +6 -0
  25. package/lib/runtime-store.js +7 -0
  26. package/lib/runtime-store.js.map +1 -0
  27. package/lib/session-service.d.ts +2 -4
  28. package/lib/session-service.js +4 -10
  29. package/lib/session-service.js.map +1 -1
  30. package/lib/view.d.ts +2 -3
  31. package/lib/view.js +6 -4
  32. package/lib/view.js.map +1 -1
  33. package/middlewares/riddle-alias.ts +18 -0
  34. package/middlewares/riddle-text.ts +75 -0
  35. package/package.json +45 -10
  36. package/plugin.ts +64 -0
  37. package/schema.json +6 -0
  38. package/src/game-flow.ts +28 -24
  39. package/src/index.ts +9 -32
  40. package/src/memory-db.ts +22 -0
  41. package/src/models.ts +7 -4
  42. package/src/riddle-command.ts +14 -5
  43. package/src/riddle-provider.ts +2 -1
  44. package/src/riddles-catalog.ts +8 -0
  45. package/src/runtime-store.ts +9 -0
  46. package/src/session-service.ts +5 -10
  47. package/src/view.ts +7 -4
  48. package/lib/commands.d.ts +0 -6
  49. package/lib/commands.d.ts.map +0 -1
  50. package/lib/commands.js +0 -94
  51. package/lib/commands.js.map +0 -1
  52. package/lib/data/riddles.d.ts +0 -3
  53. package/lib/data/riddles.d.ts.map +0 -1
  54. package/lib/data/riddles.js +0 -2
  55. package/lib/data/riddles.js.map +0 -1
  56. package/lib/engine.d.ts.map +0 -1
  57. package/lib/game-flow.d.ts.map +0 -1
  58. package/lib/hub-register.d.ts +0 -5
  59. package/lib/hub-register.d.ts.map +0 -1
  60. package/lib/hub-register.js +0 -32
  61. package/lib/hub-register.js.map +0 -1
  62. package/lib/index.d.ts.map +0 -1
  63. package/lib/models.d.ts.map +0 -1
  64. package/lib/riddle-command.d.ts.map +0 -1
  65. package/lib/riddle-provider.d.ts.map +0 -1
  66. package/lib/session-service.d.ts.map +0 -1
  67. package/lib/view.d.ts.map +0 -1
  68. package/plugin.yml +0 -2
  69. package/src/commands.ts +0 -112
  70. package/src/data/riddles.ts +0 -8
  71. package/src/hub-register.ts +0 -34
@@ -0,0 +1,75 @@
1
+ import { defineMiddleware } from '@zhin.js/middleware';
2
+ import type { Message } from '@zhin.js/core/runtime';
3
+ import {
4
+ buildChoiceFallbackMap,
5
+ channelKey,
6
+ messageFromCommandInput,
7
+ parseChoicePayload,
8
+ resolveGameTextPayload,
9
+ } from '@zhin.js/game-kit';
10
+ import { resolveGameServices } from '../src/runtime-store.js';
11
+ import { handleChoice, processAnswerText, RIDDLE_PREFIX } from '../src/game-flow.js';
12
+
13
+ /**
14
+ * 文本入口:按钮 payload(`riddle:session:choiceId`)、数字 fallback
15
+ * (『1提示 2跳过 3结束』,对应 view 的 fallbackHint),其余按谜底作答。
16
+ */
17
+ export default defineMiddleware<Message>({
18
+ target: 'inbound',
19
+ async handle(context, next) {
20
+ const raw = context.input.content?.trim() ?? '';
21
+ if (!raw) {
22
+ await next();
23
+ return;
24
+ }
25
+ const services = resolveGameServices(context);
26
+ const message = messageFromCommandInput(context.input);
27
+ const ch = channelKey(message);
28
+
29
+ // 直接 payload(QQ 指令预填 / Sandbox action→text)
30
+ const payloadFromText = resolveGameTextPayload(raw);
31
+ if (payloadFromText?.startsWith(`${RIDDLE_PREFIX}:`)) {
32
+ const parsed = parseChoicePayload(payloadFromText, RIDDLE_PREFIX);
33
+ if (parsed) {
34
+ const session = await services.getById(parsed.sessionId);
35
+ if (session?.channel_key === ch) {
36
+ const reply = await handleChoice(null, services, message, parsed.sessionId, parsed.choiceId);
37
+ if (reply) await context.input.$reply(reply);
38
+ return;
39
+ }
40
+ }
41
+ await next();
42
+ return;
43
+ }
44
+
45
+ const session = await services.getActiveForUser(ch, message.$sender.id);
46
+ if (!session) {
47
+ await next();
48
+ return;
49
+ }
50
+
51
+ if (session.status === 'active') {
52
+ const map = buildChoiceFallbackMap(RIDDLE_PREFIX, session.id, [
53
+ { id: 'hint', label: '提示' },
54
+ { id: 'skip', label: '跳过' },
55
+ { id: 'quit', label: '结束' },
56
+ ]);
57
+ const payload = resolveGameTextPayload(raw, map);
58
+ const parsed = payload ? parseChoicePayload(payload, RIDDLE_PREFIX) : null;
59
+ if (parsed?.sessionId === session.id) {
60
+ const reply = await handleChoice(null, services, message, session.id, parsed.choiceId);
61
+ if (reply) await context.input.$reply(reply);
62
+ return;
63
+ }
64
+ }
65
+
66
+ // 仅把 1-8 个汉字的纯文本当谜底作答,其余放行
67
+ if (/^[\u4e00-\u9fff]{1,8}$/.test(raw)) {
68
+ const reply = await processAnswerText(null, services, message, raw);
69
+ if (reply) await context.input.$reply(reply);
70
+ return;
71
+ }
72
+
73
+ await next();
74
+ },
75
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-word-riddle",
3
- "version": "1.0.0",
4
- "description": "Zhin.js 猜谜 — 字谜与猜成语",
3
+ "version": "1.0.2",
4
+ "description": "Zhin.js 猜谜 — 字谜与猜成语 (Plugin Runtime)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -14,26 +14,61 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
+ "plugin.ts",
18
+ "schema.json",
19
+ "commands",
20
+ "middlewares",
17
21
  "src",
18
22
  "lib",
19
- "plugin.yml",
20
23
  "README.md"
21
24
  ],
22
25
  "license": "MIT",
23
- "peerDependencies": {
24
- "zhin.js": "4.1.0"
25
- },
26
26
  "dependencies": {
27
27
  "chinese-idiom-chengyu": "^1.1.0",
28
- "@zhin.js/game-shared": "1.0.0"
28
+ "@zhin.js/command": "1.0.1",
29
+ "@zhin.js/core": "1.3.5",
30
+ "@zhin.js/game-kit": "1.0.2",
31
+ "@zhin.js/middleware": "1.0.1",
32
+ "@zhin.js/plugin-runtime": "1.0.1"
29
33
  },
30
34
  "devDependencies": {
31
35
  "typescript": "^6.0.3",
32
- "zhin.js": "4.1.0"
36
+ "vitest": "^4.1.10"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/zhinjs/zhin.git",
41
+ "directory": "plugins/games/word-riddle"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org"
46
+ },
47
+ "engines": {
48
+ "node": "^20.19.0 || >=22.12.0"
49
+ },
50
+ "zhin": {
51
+ "protocol": 1,
52
+ "type": "plugin",
53
+ "entry": "./plugin.ts",
54
+ "engine": "^1.0.0",
55
+ "runtime": "trusted",
56
+ "features": [
57
+ {
58
+ "package": "@zhin.js/command",
59
+ "api": "^1.0.0"
60
+ },
61
+ {
62
+ "package": "@zhin.js/middleware",
63
+ "api": "^1.0.0"
64
+ }
65
+ ],
66
+ "plugins": []
33
67
  },
34
68
  "scripts": {
35
69
  "build:data": "node scripts/build-char-riddles.mjs",
36
- "build": "pnpm run build:data && tsc --build && node -e \"import('node:fs/promises').then(fs=>fs.mkdir('lib/data',{recursive:true}).then(()=>fs.copyFile('src/data/char-riddles.json','lib/data/char-riddles.json')))\"",
37
- "clean": "rimraf lib"
70
+ "build": "pnpm run build:data && tsc && node -e \"import('node:fs/promises').then(fs=>fs.mkdir('lib/data',{recursive:true}).then(()=>fs.copyFile('src/data/char-riddles.json','lib/data/char-riddles.json')))\"",
71
+ "clean": "rimraf lib",
72
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/games/word-riddle/tests"
38
73
  }
39
74
  }
package/plugin.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { definePlugin, databaseHostToken, scheduleHostToken } from '@zhin.js/plugin-runtime';
2
+ import {
3
+ registerRuntimeGame,
4
+ initGameRecordHost,
5
+ DEFAULT_GAME_STALE_CRON,
6
+ DEFAULT_GAME_STALE_IDLE_MS,
7
+ } from '@zhin.js/game-kit';
8
+ import { mountRiddleHostServices, mountRiddleMemoryServices } from './src/memory-db.js';
9
+ import { gameServicesToken } from './src/runtime-store.js';
10
+ import type { SessionService } from './src/session-service.js';
11
+
12
+ /**
13
+ * Plugin Runtime:
14
+ * - Commands under `commands/` are authoritative (help + start/continue/quit via in-memory DB).
15
+ * - DB: prefer databaseHostToken; else in-memory SessionService.
16
+ * - Text middleware under `middlewares/` handles answer payloads.
17
+ */
18
+ export default definePlugin({
19
+ name: 'word-riddle',
20
+ metadata: {
21
+ displayName: 'Word Riddle',
22
+ },
23
+ setup(context) {
24
+ let services: SessionService;
25
+ if (context.resources.has(databaseHostToken)) {
26
+ const host = context.resources.use(databaseHostToken);
27
+ services = mountRiddleHostServices(host);
28
+ context.lifecycle.add(initGameRecordHost(host));
29
+ } else {
30
+ services = mountRiddleMemoryServices();
31
+ }
32
+ context.resources.provide(gameServicesToken, services);
33
+ const disposeHub = registerRuntimeGame({
34
+ id: 'riddle',
35
+ title: '猜谜',
36
+ icon: '🧩',
37
+ description: '字谜 + 成语猜谜',
38
+ commandPrefix: '/猜谜',
39
+ quickStart: '开始',
40
+ aliases: ['riddle'],
41
+ menus: [
42
+ { id: 'char', label: '🔤 字谜模式' },
43
+ { id: 'idiom', label: '📜 成语模式' },
44
+ { id: 'continue', label: '🔄 继续' },
45
+ { id: 'help', label: '📖 玩法说明' },
46
+ ],
47
+ });
48
+ context.lifecycle.add(disposeHub);
49
+
50
+ if (context.resources.has(scheduleHostToken)) {
51
+ const schedule = context.resources.use(scheduleHostToken);
52
+ const disposeCron = schedule.register({
53
+ id: 'riddle/abort-stale',
54
+ cron: DEFAULT_GAME_STALE_CRON,
55
+ description: 'Abort stale word-riddle sessions',
56
+ async execute() {
57
+ if (!services.abortStale) return;
58
+ await services.abortStale(DEFAULT_GAME_STALE_IDLE_MS);
59
+ },
60
+ });
61
+ context.lifecycle.add(disposeCron);
62
+ }
63
+ },
64
+ });
package/schema.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {}
6
+ }
package/src/game-flow.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Adapter, Message, Plugin } from 'zhin.js';
1
+ import type { Adapter, Message, Plugin } from '@zhin.js/core';
2
+ import { plainTextFromSendContent } from '@zhin.js/game-kit';
2
3
  import {
3
4
  checkAnswer,
4
5
  getRiddleById,
@@ -6,7 +7,7 @@ import {
6
7
  typeLabel,
7
8
  } from './engine.js';
8
9
  import type { RiddleSessionRow } from './models.js';
9
- import type { RiddleType } from './data/riddles.js';
10
+ import type { RiddleType } from './riddles-catalog.js';
10
11
  import {
11
12
  currentRiddleId,
12
13
  parseQueue,
@@ -15,17 +16,19 @@ import {
15
16
  import { buildRiddleView, MAX_WRONG } from './view.js';
16
17
 
17
18
  export async function sendOrEditView(
18
- plugin: Plugin,
19
+ plugin: Plugin | null,
19
20
  services: SessionService,
20
21
  message: Message<any>,
21
22
  session: RiddleSessionRow,
22
23
  eventLines: string[] = [],
23
- ): Promise<void> {
24
- const content = buildRiddleView(session, eventLines);
24
+ ): Promise<string | void> {
25
+ const content = buildRiddleView(session, eventLines, message.$channel.type);
25
26
  if (typeof content === 'string') {
27
+ if (!plugin) return content;
26
28
  await message.$reply?.(content);
27
29
  return;
28
30
  }
31
+ if (!plugin) return plainTextFromSendContent(content);
29
32
 
30
33
  const adapter = plugin.root.inject(message.$adapter) as Adapter;
31
34
  if (session.board_message_id) {
@@ -62,7 +65,7 @@ async function advanceQuestion(
62
65
  }
63
66
 
64
67
  export async function startGame(
65
- plugin: Plugin,
68
+ plugin: Plugin | null,
66
69
  services: SessionService,
67
70
  message: Message<any>,
68
71
  mode: RiddleType,
@@ -77,12 +80,12 @@ export async function startGame(
77
80
  }
78
81
 
79
82
  const session = await services.createSession(message, mode);
80
- await sendOrEditView(plugin, services, message, session);
81
- return undefined;
83
+ const text = await sendOrEditView(plugin, services, message, session);
84
+ return typeof text === 'string' ? text : undefined;
82
85
  }
83
86
 
84
87
  export async function continueGame(
85
- plugin: Plugin,
88
+ plugin: Plugin | null,
86
89
  services: SessionService,
87
90
  message: Message<any>,
88
91
  ): Promise<string> {
@@ -91,12 +94,13 @@ export async function continueGame(
91
94
  message.$sender.id,
92
95
  );
93
96
  if (!session) return '你没有进行中的猜谜,发送「猜谜 开始」。';
94
- await sendOrEditView(plugin, services, message, session);
97
+ const text = await sendOrEditView(plugin, services, message, session);
98
+ if (typeof text === 'string') return text;
95
99
  return '已刷新猜谜界面。';
96
100
  }
97
101
 
98
102
  export async function processAnswerText(
99
- plugin: Plugin,
103
+ plugin: Plugin | null,
100
104
  services: SessionService,
101
105
  message: Message<any>,
102
106
  raw: string,
@@ -120,32 +124,29 @@ export async function processAnswerText(
120
124
  });
121
125
  const after = await advanceQuestion(services, (await services.getById(session.id))!);
122
126
  const explain = entry.explanation ? `\n📖 ${entry.explanation}` : '';
123
- await sendOrEditView(plugin, services, message, after, [
127
+ return (await sendOrEditView(plugin, services, message, after, [
124
128
  `✅ 正确!答案:**${entry.answer}**${explain}`,
125
129
  `+${10 + Math.min(streak, 5)} 分`,
126
- ]);
127
- return null;
130
+ ])) ?? null;
128
131
  }
129
132
 
130
133
  const wrong = session.wrong_count + 1;
131
134
  if (wrong >= MAX_WRONG) {
132
135
  await services.updateSession(session.id, { wrong_count: wrong, streak: 0 });
133
136
  const after = await advanceQuestion(services, (await services.getById(session.id))!);
134
- await sendOrEditView(plugin, services, message, after, [
137
+ return (await sendOrEditView(plugin, services, message, after, [
135
138
  `❌ 本题答案:**${entry.answer}**`,
136
139
  '失误过多,自动下一题。',
137
- ]);
138
- return null;
140
+ ])) ?? null;
139
141
  }
140
142
 
141
143
  await services.updateSession(session.id, { wrong_count: wrong, streak: 0 });
142
144
  const updated = (await services.getById(session.id))!;
143
- await sendOrEditView(plugin, services, message, updated, ['❌ 不对,再想想!']);
144
- return null;
145
+ return (await sendOrEditView(plugin, services, message, updated, ['❌ 不对,再想想!'])) ?? null;
145
146
  }
146
147
 
147
148
  export async function handleChoice(
148
- plugin: Plugin,
149
+ plugin: Plugin | null,
149
150
  services: SessionService,
150
151
  message: Message<any>,
151
152
  sessionId: string,
@@ -174,15 +175,18 @@ export async function handleChoice(
174
175
  const hint = entry.hint ?? `答案共 ${entry.answer.length} 个字`;
175
176
  await services.updateSession(session.id, { hints_used: session.hints_used + 1, streak: 0 });
176
177
  const updated = (await services.getById(session.id))!;
177
- await sendOrEditView(plugin, services, message, updated, [`💡 提示:${hint}`, '(连击清零)']);
178
- return null;
178
+ return (await sendOrEditView(plugin, services, message, updated, [
179
+ `💡 提示:${hint}`,
180
+ '(连击清零)',
181
+ ])) ?? null;
179
182
  }
180
183
 
181
184
  if (choiceId === 'skip') {
182
185
  await services.updateSession(session.id, { streak: 0 });
183
186
  const after = await advanceQuestion(services, session);
184
- await sendOrEditView(plugin, services, message, after, [`⏭️ 跳过,答案:**${entry.answer}**`]);
185
- return null;
187
+ return (await sendOrEditView(plugin, services, message, after, [
188
+ `⏭️ 跳过,答案:**${entry.answer}**`,
189
+ ])) ?? null;
186
190
  }
187
191
 
188
192
  if (choiceId === 'quit') {
package/src/index.ts CHANGED
@@ -1,32 +1,9 @@
1
- import { Cron, formatCompact, usePlugin, type DatabaseFeature } from 'zhin.js';
2
- import { registerModels } from './models.js';
3
- import { createServices, resolveGameDatabase, type SessionService } from './session-service.js';
4
- import { registerCommands, registerInteractive, registerTextMiddleware } from './commands.js';
5
- import { registerRiddleHub } from './hub-register.js';
6
-
7
- const plugin = usePlugin();
8
- const { logger, useContext, addCron } = plugin;
9
-
10
- registerModels(plugin);
11
-
12
- let services: SessionService | null = null;
13
-
14
- useContext('database', (dbFeature: DatabaseFeature) => {
15
- services = createServices(resolveGameDatabase(dbFeature));
16
- logger.info(formatCompact({ 模块: '猜谜', 数据模型: '已就绪' }));
17
- });
18
-
19
- registerRiddleHub(() => services);
20
- registerCommands(plugin, () => services);
21
- registerInteractive(plugin, () => services);
22
- registerTextMiddleware(plugin, () => services);
23
-
24
- addCron(
25
- new Cron('0 */15 * * * *', async () => {
26
- if (!services) return;
27
- const n = await services.abortStale(45 * 60 * 1000);
28
- if (n > 0) logger.debug(formatCompact({ 猜谜: '清理超时局', count: n }));
29
- }),
30
- );
31
-
32
- logger.info(formatCompact({ 模块: '猜谜', 状态: '已加载' }));
1
+ export { RIDDLE_HELP } from './riddle-command.js';
2
+ export { gameServicesToken, resolveGameServices } from './runtime-store.js';
3
+ export { createInMemoryRiddleDb, mountRiddleMemoryServices } from './memory-db.js';
4
+
5
+ /**
6
+ * 已由 plugin.ts 接线:commands/ 命令、middlewares/ 文本与选项中间件、
7
+ * 游戏大厅注册(registerRuntimeGame)、过期会话 cron(scheduleHostToken)、
8
+ * host DatabaseHost(databaseHostToken)。仍未接:interactive 按钮回调。
9
+ */
@@ -0,0 +1,22 @@
1
+ import { createHostGameDb, createInMemoryGameDb } from '@zhin.js/game-kit';
2
+ import type { DatabaseHost } from '@zhin.js/plugin-runtime';
3
+ import { defineHostTables } from './models.js';
4
+ import { createServices, type RiddleDatabase, type SessionService } from './session-service.js';
5
+
6
+ const RIDDLE_TABLES = ['word_riddle_sessions'] as const;
7
+
8
+ /** In-memory word_riddle_sessions for Plugin Runtime slice-2 (no DatabaseFeature). */
9
+ export function createInMemoryRiddleDb(): RiddleDatabase {
10
+ return createInMemoryGameDb(RIDDLE_TABLES) as unknown as RiddleDatabase;
11
+ }
12
+
13
+ export function mountRiddleMemoryServices(): SessionService {
14
+ const services = createServices(createInMemoryRiddleDb());
15
+ return services;
16
+ }
17
+
18
+ export function mountRiddleHostServices(host: DatabaseHost): SessionService {
19
+ defineHostTables(host);
20
+ const services = createServices(createHostGameDb(host, RIDDLE_TABLES) as unknown as RiddleDatabase);
21
+ return services;
22
+ }
package/src/models.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { Models, Plugin } from 'zhin.js';
1
+ import type { Models } from '@zhin.js/core';
2
2
 
3
3
  export type RiddleSessionStatus = 'active' | 'completed' | 'aborted';
4
4
 
5
- declare module 'zhin.js' {
5
+ declare module '@zhin.js/core' {
6
6
  interface Models {
7
7
  word_riddle_sessions: {
8
8
  id: string;
@@ -31,8 +31,11 @@ declare module 'zhin.js' {
31
31
 
32
32
  export type RiddleSessionRow = Models['word_riddle_sessions'];
33
33
 
34
- export function registerModels(plugin: Plugin): void {
35
- plugin.defineModel('word_riddle_sessions', {
34
+
35
+ export function defineHostTables(
36
+ db: { define: (name: string, definition: Record<string, unknown>) => void },
37
+ ): void {
38
+ db.define('word_riddle_sessions', {
36
39
  id: { type: 'text', primary: true },
37
40
  adapter: { type: 'text', nullable: false },
38
41
  endpoint: { type: 'text', nullable: false },
@@ -1,8 +1,8 @@
1
- import type { Message, Plugin } from 'zhin.js';
2
- import { channelKey } from '@zhin.js/game-shared';
1
+ import type { Message, Plugin } from '@zhin.js/core';
2
+ import { channelKey } from '@zhin.js/game-kit';
3
3
  import { continueGame, startGame } from './game-flow.js';
4
- import { riddleCount } from './data/riddles.js';
5
- import type { RiddleType } from './data/riddles.js';
4
+ import { riddleCount, type RiddleType } from './riddles-catalog.js';
5
+
6
6
  import { typeLabel } from './engine.js';
7
7
  import type { SessionService } from './session-service.js';
8
8
 
@@ -29,7 +29,7 @@ function parseMode(action: string): RiddleType | null {
29
29
  }
30
30
 
31
31
  export async function runRiddleCommand(
32
- plugin: Plugin,
32
+ plugin: Plugin | null,
33
33
  services: SessionService,
34
34
  message: Message<any>,
35
35
  action: string,
@@ -64,3 +64,12 @@ export async function runRiddleCommand(
64
64
 
65
65
  return `未知子命令:${action}\n\n${RIDDLE_HELP}`;
66
66
  }
67
+
68
+ /** Plugin Runtime / smoke: text-only, no Adapter.editMessage. */
69
+ export async function runRiddleCommandText(
70
+ services: SessionService,
71
+ message: Message<any>,
72
+ action: string,
73
+ ): Promise<string> {
74
+ return (await runRiddleCommand(null, services, message, action)) ?? '';
75
+ }
@@ -7,6 +7,7 @@ import { readFileSync } from 'node:fs';
7
7
  import { dirname, join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { createRequire } from 'node:module';
10
+ import { secureRandomInt } from '@zhin.js/game-kit';
10
11
 
11
12
  export type RiddleType = 'char' | 'idiom';
12
13
 
@@ -98,7 +99,7 @@ export function pickRoundQueue(type: RiddleType, count = QUESTIONS_PER_ROUND): R
98
99
  const k = Math.min(count, n);
99
100
  const indices = Array.from({ length: n }, (_, i) => i);
100
101
  for (let i = 0; i < k; i++) {
101
- const j = i + Math.floor(Math.random() * (n - i));
102
+ const j = i + secureRandomInt(n - i);
102
103
  [indices[i], indices[j]] = [indices[j]!, indices[i]!];
103
104
  }
104
105
  return indices.slice(0, k).map((i) => pool[i]!);
@@ -0,0 +1,8 @@
1
+ export type { RiddleEntry, RiddleType } from './riddle-provider.js';
2
+ export {
3
+ getRiddleById,
4
+ pickRoundQueue,
5
+ QUESTIONS_PER_ROUND,
6
+ riddleCount,
7
+ riddlesByType,
8
+ } from './riddle-provider.js';
@@ -0,0 +1,9 @@
1
+ import { createToken } from '@zhin.js/plugin-runtime';
2
+ import type { SessionService } from './session-service.js';
3
+
4
+ /** Owner-scoped service consumed by discovered commands and middleware. */
5
+ export const gameServicesToken = createToken<SessionService>('zhin.game.word-riddle.services');
6
+
7
+ export function resolveGameServices(context: { use<T>(token: typeof gameServicesToken): T }): SessionService {
8
+ return context.use(gameServicesToken) as SessionService;
9
+ }
@@ -1,6 +1,6 @@
1
- import type { Database, DatabaseFeature, Message, Models, RelatedModel } from 'zhin.js';
2
- import { channelKey, generateSessionId } from '@zhin.js/game-shared';
3
- import { pickRoundQueue, type RiddleType } from './data/riddles.js';
1
+ import type { Database, Message, Models, RelatedModel } from '@zhin.js/core';
2
+ import { channelKey, generateSessionId, boardMessageMatches } from '@zhin.js/game-kit';
3
+ import { pickRoundQueue, type RiddleType } from './riddles-catalog.js';
4
4
  import type { RiddleSessionRow } from './models.js';
5
5
 
6
6
  export type RiddleDatabase = Database<unknown, Models, string>;
@@ -42,11 +42,9 @@ export class SessionService {
42
42
 
43
43
  async getActiveByBoardMessageId(messageId: string): Promise<RiddleSessionRow | null> {
44
44
  if (!messageId) return null;
45
- const rows = await getModel(this.db).findAll({ status: 'active' });
45
+ const rows = await getModel(this.db).findAll({});
46
46
  for (const row of rows) {
47
- const stored = row.board_message_id;
48
- if (!stored) continue;
49
- if (stored === messageId || stored.endsWith(`:${messageId}`)) return row;
47
+ if (boardMessageMatches(row.board_message_id ?? '', messageId)) return row;
50
48
  }
51
49
  return null;
52
50
  }
@@ -103,9 +101,6 @@ export function createServices(db: RiddleDatabase): SessionService {
103
101
  return new SessionService(db);
104
102
  }
105
103
 
106
- export function resolveGameDatabase(feature: DatabaseFeature): RiddleDatabase {
107
- return feature.db;
108
- }
109
104
 
110
105
  export function currentRiddleId(session: RiddleSessionRow): string | null {
111
106
  const queue = parseQueue(session.queue);
package/src/view.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { buildChoiceKeyboard } from '@zhin.js/game-shared';
2
- import type { SendContent } from 'zhin.js';
1
+ import { buildChoiceKeyboard } from '@zhin.js/game-kit';
2
+ import type { SendContent } from '@zhin.js/core';
3
3
  import { getRiddleById, RIDDLE_PREFIX, typeLabel } from './engine.js';
4
4
  import type { RiddleSessionRow } from './models.js';
5
5
  import { currentRiddleId, parseQueue } from './session-service.js';
@@ -9,6 +9,7 @@ const MAX_WRONG = 3;
9
9
  export function buildRiddleView(
10
10
  session: RiddleSessionRow,
11
11
  eventLines: string[] = [],
12
+ channelType?: string,
12
13
  ): SendContent | string {
13
14
  const queue = parseQueue(session.queue);
14
15
  const terminal = session.status !== 'active';
@@ -37,8 +38,8 @@ export function buildRiddleView(
37
38
 
38
39
  const choices = terminal
39
40
  ? [
40
- { id: 'restart_char', label: '🔤 再来字谜', style: 'primary' as const },
41
- { id: 'restart_idiom', label: '📜 再来成语', style: 'primary' as const },
41
+ { id: 'restart_char', label: '🔤 再来字谜', style: 'primary' as const, keepEnabledWhenTerminal: true },
42
+ { id: 'restart_idiom', label: '📜 再来成语', style: 'primary' as const, keepEnabledWhenTerminal: true },
42
43
  ]
43
44
  : [
44
45
  { id: 'hint', label: '💡 提示', style: 'secondary' as const },
@@ -54,6 +55,8 @@ export function buildRiddleView(
54
55
  terminal,
55
56
  buttonsPerRow: terminal ? 2 : 3,
56
57
  fallbackHint: '回复答案,或 1提示 2跳过 3结束',
58
+ interactionProfile: terminal ? 'terminal' : 'gameplay',
59
+ channelType,
57
60
  });
58
61
  }
59
62
 
package/lib/commands.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type Plugin } from 'zhin.js';
2
- import type { SessionService } from './session-service.js';
3
- export declare function registerCommands(plugin: Plugin, getServices: () => SessionService | null): void;
4
- export declare function registerInteractive(plugin: Plugin, getServices: () => SessionService | null): void;
5
- export declare function registerTextMiddleware(plugin: Plugin, getServices: () => SessionService | null): void;
6
- //# sourceMappingURL=commands.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAUrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAoB3D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAG/F;AA4BD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAUlG;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAsCrG"}