@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.
- package/commands/riddle/[action:string=].ts +15 -0
- package/lib/engine.d.ts +0 -1
- package/lib/game-flow.d.ts +7 -8
- package/lib/game-flow.js +23 -16
- package/lib/game-flow.js.map +1 -1
- package/lib/index.d.ts +8 -2
- package/lib/index.js +8 -25
- package/lib/index.js.map +1 -1
- package/lib/memory-db.d.ts +6 -0
- package/lib/memory-db.js +18 -0
- package/lib/memory-db.js.map +1 -0
- package/lib/models.d.ts +5 -4
- package/lib/models.js +2 -2
- package/lib/models.js.map +1 -1
- package/lib/riddle-command.d.ts +4 -3
- package/lib/riddle-command.js +6 -2
- package/lib/riddle-command.js.map +1 -1
- package/lib/riddle-provider.d.ts +0 -1
- package/lib/riddle-provider.js +2 -1
- package/lib/riddle-provider.js.map +1 -1
- package/lib/riddles-catalog.d.ts +2 -0
- package/lib/riddles-catalog.js +2 -0
- package/lib/riddles-catalog.js.map +1 -0
- package/lib/runtime-store.d.ts +6 -0
- package/lib/runtime-store.js +7 -0
- package/lib/runtime-store.js.map +1 -0
- package/lib/session-service.d.ts +2 -4
- package/lib/session-service.js +4 -10
- package/lib/session-service.js.map +1 -1
- package/lib/view.d.ts +2 -3
- package/lib/view.js +6 -4
- package/lib/view.js.map +1 -1
- package/middlewares/riddle-alias.ts +18 -0
- package/middlewares/riddle-text.ts +75 -0
- package/package.json +45 -10
- package/plugin.ts +64 -0
- package/schema.json +6 -0
- package/src/game-flow.ts +28 -24
- package/src/index.ts +9 -32
- package/src/memory-db.ts +22 -0
- package/src/models.ts +7 -4
- package/src/riddle-command.ts +14 -5
- package/src/riddle-provider.ts +2 -1
- package/src/riddles-catalog.ts +8 -0
- package/src/runtime-store.ts +9 -0
- package/src/session-service.ts +5 -10
- package/src/view.ts +7 -4
- package/lib/commands.d.ts +0 -6
- package/lib/commands.d.ts.map +0 -1
- package/lib/commands.js +0 -94
- package/lib/commands.js.map +0 -1
- package/lib/data/riddles.d.ts +0 -3
- package/lib/data/riddles.d.ts.map +0 -1
- package/lib/data/riddles.js +0 -2
- package/lib/data/riddles.js.map +0 -1
- package/lib/engine.d.ts.map +0 -1
- package/lib/game-flow.d.ts.map +0 -1
- package/lib/hub-register.d.ts +0 -5
- package/lib/hub-register.d.ts.map +0 -1
- package/lib/hub-register.js +0 -32
- package/lib/hub-register.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/models.d.ts.map +0 -1
- package/lib/riddle-command.d.ts.map +0 -1
- package/lib/riddle-provider.d.ts.map +0 -1
- package/lib/session-service.d.ts.map +0 -1
- package/lib/view.d.ts.map +0 -1
- package/plugin.yml +0 -2
- package/src/commands.ts +0 -112
- package/src/data/riddles.ts +0 -8
- 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.
|
|
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/
|
|
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
|
-
"
|
|
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
|
|
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
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 './
|
|
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, [
|
|
178
|
-
|
|
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, [
|
|
185
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
*/
|
package/src/memory-db.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
35
|
-
|
|
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 },
|
package/src/riddle-command.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Message, Plugin } from 'zhin.js';
|
|
2
|
-
import { channelKey } from '@zhin.js/game-
|
|
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 './
|
|
5
|
-
|
|
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
|
+
}
|
package/src/riddle-provider.ts
CHANGED
|
@@ -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 +
|
|
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,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
|
+
}
|
package/src/session-service.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Database,
|
|
2
|
-
import { channelKey, generateSessionId } from '@zhin.js/game-
|
|
3
|
-
import { pickRoundQueue, type RiddleType } from './
|
|
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({
|
|
45
|
+
const rows = await getModel(this.db).findAll({});
|
|
46
46
|
for (const row of rows) {
|
|
47
|
-
|
|
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-
|
|
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
|
package/lib/commands.d.ts.map
DELETED
|
@@ -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"}
|