@zhin.js/plugin-idiom-chain 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/README.md +31 -0
- package/commands/chain/[action:string=].ts +15 -0
- package/lib/chain-command.d.ts +4 -3
- package/lib/chain-command.js +5 -1
- package/lib/chain-command.js.map +1 -1
- package/lib/engine.d.ts +0 -1
- package/lib/game-flow.d.ts +6 -7
- package/lib/game-flow.js +27 -22
- package/lib/game-flow.js.map +1 -1
- package/lib/idiom-provider.d.ts +0 -1
- package/lib/idiom-provider.js +3 -2
- package/lib/idiom-provider.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/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 +1 -3
- package/lib/session-service.js +3 -9
- package/lib/session-service.js.map +1 -1
- package/lib/view.d.ts +2 -3
- package/lib/view.js +5 -3
- package/lib/view.js.map +1 -1
- package/middlewares/chain-alias.ts +18 -0
- package/middlewares/chain-text.ts +75 -0
- package/package.json +45 -10
- package/plugin.ts +64 -0
- package/schema.json +6 -0
- package/src/chain-command.ts +14 -5
- package/src/game-flow.ts +33 -29
- package/src/idiom-provider.ts +3 -2
- package/src/index.ts +9 -32
- package/src/memory-db.ts +22 -0
- package/src/models.ts +7 -4
- package/src/runtime-store.ts +9 -0
- package/src/session-service.ts +4 -9
- package/src/view.ts +6 -3
- package/lib/chain-command.d.ts.map +0 -1
- 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/idioms.d.ts +0 -8
- package/lib/data/idioms.d.ts.map +0 -1
- package/lib/data/idioms.js +0 -422
- package/lib/data/idioms.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 -31
- package/lib/hub-register.js.map +0 -1
- package/lib/idiom-provider.d.ts.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/models.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/hub-register.ts +0 -32
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineGameCommandAliasMiddleware,
|
|
3
|
+
messageFromCommandInput,
|
|
4
|
+
normalizeChainAction,
|
|
5
|
+
} from '@zhin.js/game-kit';
|
|
6
|
+
import { CHAIN_HELP, runChainCommandText } from '../src/chain-command.js';
|
|
7
|
+
import { resolveGameServices } from '../src/runtime-store.js';
|
|
8
|
+
|
|
9
|
+
export default defineGameCommandAliasMiddleware({
|
|
10
|
+
aliases: ['接龙', 'chain'],
|
|
11
|
+
async run(action, input, context) {
|
|
12
|
+
const normalized = normalizeChainAction(String(action ?? ''));
|
|
13
|
+
if (!normalized || normalized === 'help') return CHAIN_HELP;
|
|
14
|
+
const services = resolveGameServices(context);
|
|
15
|
+
const message = messageFromCommandInput(input);
|
|
16
|
+
return runChainCommandText(services, message, normalized);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -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 { CHAIN_PREFIX, handleChoice, processIdiomText } from '../src/game-flow.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 文本入口:按钮 payload(`chain: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(`${CHAIN_PREFIX}:`)) {
|
|
32
|
+
const parsed = parseChoicePayload(payloadFromText, CHAIN_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(CHAIN_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, CHAIN_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
|
+
// 仅把 2-8 个汉字的纯文本当成语作答,其余放行
|
|
67
|
+
if (/^[\u4e00-\u9fff]{2,8}$/.test(raw)) {
|
|
68
|
+
const reply = await processIdiomText(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-idiom-chain",
|
|
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,25 +14,60 @@
|
|
|
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/idiom-chain"
|
|
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
|
-
"build": "tsc
|
|
36
|
-
"clean": "rimraf lib"
|
|
69
|
+
"build": "tsc",
|
|
70
|
+
"clean": "rimraf lib",
|
|
71
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/games/idiom-chain/tests"
|
|
37
72
|
}
|
|
38
73
|
}
|
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 { mountChainHostServices, mountChainMemoryServices } 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 idiom answer payloads.
|
|
17
|
+
*/
|
|
18
|
+
export default definePlugin({
|
|
19
|
+
name: 'idiom-chain',
|
|
20
|
+
metadata: {
|
|
21
|
+
displayName: 'Idiom Chain',
|
|
22
|
+
},
|
|
23
|
+
setup(context) {
|
|
24
|
+
let services: SessionService;
|
|
25
|
+
if (context.resources.has(databaseHostToken)) {
|
|
26
|
+
const host = context.resources.use(databaseHostToken);
|
|
27
|
+
services = mountChainHostServices(host);
|
|
28
|
+
context.lifecycle.add(initGameRecordHost(host));
|
|
29
|
+
} else {
|
|
30
|
+
services = mountChainMemoryServices();
|
|
31
|
+
}
|
|
32
|
+
context.resources.provide(gameServicesToken, services);
|
|
33
|
+
const disposeHub = registerRuntimeGame({
|
|
34
|
+
id: 'chain',
|
|
35
|
+
title: '成语接龙',
|
|
36
|
+
icon: '📜',
|
|
37
|
+
description: '四字成语接龙(同音/同字)',
|
|
38
|
+
commandPrefix: '/接龙',
|
|
39
|
+
quickStart: 'start_pinyin',
|
|
40
|
+
aliases: ['chain'],
|
|
41
|
+
menus: [
|
|
42
|
+
{ id: 'start_pinyin', label: '🎮 同音接龙' },
|
|
43
|
+
{ id: 'start_char', 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: 'chain/abort-stale',
|
|
54
|
+
cron: DEFAULT_GAME_STALE_CRON,
|
|
55
|
+
description: 'Abort stale idiom-chain 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/chain-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 { idiomCount, modeLabel, promptLine } from './engine.js';
|
|
5
|
-
|
|
4
|
+
import { idiomCount, modeLabel, promptLine, type MatchMode } from './engine.js';
|
|
5
|
+
|
|
6
6
|
import type { SessionService } from './session-service.js';
|
|
7
7
|
|
|
8
8
|
export const CHAIN_HELP = [
|
|
@@ -19,7 +19,7 @@ export const CHAIN_HELP = [
|
|
|
19
19
|
].join('\n');
|
|
20
20
|
|
|
21
21
|
export async function runChainCommand(
|
|
22
|
-
plugin: Plugin,
|
|
22
|
+
plugin: Plugin | null,
|
|
23
23
|
services: SessionService,
|
|
24
24
|
message: Message<any>,
|
|
25
25
|
action: string,
|
|
@@ -55,3 +55,12 @@ export async function runChainCommand(
|
|
|
55
55
|
|
|
56
56
|
return `未知子命令:${action}\n\n${CHAIN_HELP}`;
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/** Plugin Runtime / smoke: text-only, no Adapter.editMessage. */
|
|
60
|
+
export async function runChainCommandText(
|
|
61
|
+
services: SessionService,
|
|
62
|
+
message: Message<any>,
|
|
63
|
+
action: string,
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
return (await runChainCommand(null, services, message, action)) ?? '';
|
|
66
|
+
}
|
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
|
CHAIN_PREFIX,
|
|
4
5
|
getGloss,
|
|
@@ -21,13 +22,15 @@ function sessionMode(session: ChainSessionRow): MatchMode {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function sendOrEditView(
|
|
24
|
-
plugin: Plugin,
|
|
25
|
+
plugin: Plugin | null,
|
|
25
26
|
services: SessionService,
|
|
26
27
|
message: Message<any>,
|
|
27
28
|
session: ChainSessionRow,
|
|
28
29
|
eventLines: string[] = [],
|
|
29
|
-
): Promise<void> {
|
|
30
|
-
const content = buildChainView(session, eventLines);
|
|
30
|
+
): Promise<string | void> {
|
|
31
|
+
const content = buildChainView(session, eventLines, message.$channel.type);
|
|
32
|
+
if (!plugin) return plainTextFromSendContent(content);
|
|
33
|
+
|
|
31
34
|
const adapter = plugin.root.inject(message.$adapter) as Adapter;
|
|
32
35
|
|
|
33
36
|
if (session.board_message_id) {
|
|
@@ -83,7 +86,7 @@ async function botTurn(
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
export async function startGame(
|
|
86
|
-
plugin: Plugin,
|
|
89
|
+
plugin: Plugin | null,
|
|
87
90
|
services: SessionService,
|
|
88
91
|
message: Message<any>,
|
|
89
92
|
matchMode: MatchMode = 'pinyin',
|
|
@@ -108,15 +111,15 @@ export async function startGame(
|
|
|
108
111
|
});
|
|
109
112
|
|
|
110
113
|
const gloss = starter.gloss ?? getGloss(starter.text);
|
|
111
|
-
await sendOrEditView(plugin, services, message, session, [
|
|
114
|
+
const text = await sendOrEditView(plugin, services, message, session, [
|
|
112
115
|
`🎬 ${modeLabel(matchMode)}开局!我先出:**${starter.text}**${gloss ? `(${gloss})` : ''}`,
|
|
113
116
|
promptLine(starter.text, matchMode),
|
|
114
117
|
]);
|
|
115
|
-
return undefined;
|
|
118
|
+
return typeof text === 'string' ? text : undefined;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
export async function continueGame(
|
|
119
|
-
plugin: Plugin,
|
|
122
|
+
plugin: Plugin | null,
|
|
120
123
|
services: SessionService,
|
|
121
124
|
message: Message<any>,
|
|
122
125
|
): Promise<string> {
|
|
@@ -125,12 +128,13 @@ export async function continueGame(
|
|
|
125
128
|
message.$sender.id,
|
|
126
129
|
);
|
|
127
130
|
if (!session) return '你没有进行中的接龙,发送「接龙 开始」。';
|
|
128
|
-
await sendOrEditView(plugin, services, message, session);
|
|
131
|
+
const text = await sendOrEditView(plugin, services, message, session);
|
|
132
|
+
if (typeof text === 'string') return text;
|
|
129
133
|
return '已刷新接龙界面。';
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
export async function processIdiomText(
|
|
133
|
-
plugin: Plugin,
|
|
137
|
+
plugin: Plugin | null,
|
|
134
138
|
services: SessionService,
|
|
135
139
|
message: Message<any>,
|
|
136
140
|
raw: string,
|
|
@@ -153,13 +157,14 @@ export async function processIdiomText(
|
|
|
153
157
|
streak: 0,
|
|
154
158
|
});
|
|
155
159
|
const updated = (await services.getById(session.id))!;
|
|
156
|
-
await sendOrEditView(plugin, services, message, updated, [
|
|
157
|
-
|
|
160
|
+
return (await sendOrEditView(plugin, services, message, updated, [
|
|
161
|
+
check.reason!,
|
|
162
|
+
'失误过多,本局结束。',
|
|
163
|
+
])) ?? null;
|
|
158
164
|
}
|
|
159
165
|
await services.updateSession(session.id, { wrong_count: wrong, streak: 0 });
|
|
160
166
|
const updated = (await services.getById(session.id))!;
|
|
161
|
-
await sendOrEditView(plugin, services, message, updated, [check.reason!]);
|
|
162
|
-
return null;
|
|
167
|
+
return (await sendOrEditView(plugin, services, message, updated, [check.reason!])) ?? null;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
used.add(idiom);
|
|
@@ -181,12 +186,14 @@ export async function processIdiomText(
|
|
|
181
186
|
const userLine = `✅ 你:${idiom}${gloss ? `(${gloss})` : ''}`;
|
|
182
187
|
|
|
183
188
|
const botResult = await botTurn(services, current, used);
|
|
184
|
-
await sendOrEditView(plugin, services, message, botResult.session, [
|
|
185
|
-
|
|
189
|
+
return (await sendOrEditView(plugin, services, message, botResult.session, [
|
|
190
|
+
userLine,
|
|
191
|
+
...botResult.lines,
|
|
192
|
+
])) ?? null;
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
export async function handleChoice(
|
|
189
|
-
plugin: Plugin,
|
|
196
|
+
plugin: Plugin | null,
|
|
190
197
|
services: SessionService,
|
|
191
198
|
message: Message<any>,
|
|
192
199
|
sessionId: string,
|
|
@@ -214,11 +221,10 @@ export async function handleChoice(
|
|
|
214
221
|
});
|
|
215
222
|
const updated = (await services.getById(session.id))!;
|
|
216
223
|
const gloss = starter.gloss ?? getGloss(starter.text);
|
|
217
|
-
await sendOrEditView(plugin, services, message, updated, [
|
|
224
|
+
return (await sendOrEditView(plugin, services, message, updated, [
|
|
218
225
|
`🎬 新一局!我先出:**${starter.text}**${gloss ? `(${gloss})` : ''}`,
|
|
219
226
|
promptLine(starter.text, mode),
|
|
220
|
-
]);
|
|
221
|
-
return null;
|
|
227
|
+
])) ?? null;
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
if (session.status !== 'active') {
|
|
@@ -233,17 +239,15 @@ export async function handleChoice(
|
|
|
233
239
|
await sendOrEditView(plugin, services, message, session, ['💡 词库中暂无可用提示,你赢了!']);
|
|
234
240
|
await services.updateSession(session.id, { status: 'won', player_score: session.player_score + 1 });
|
|
235
241
|
const updated = (await services.getById(session.id))!;
|
|
236
|
-
await sendOrEditView(plugin, services, message, updated);
|
|
237
|
-
return null;
|
|
242
|
+
return (await sendOrEditView(plugin, services, message, updated)) ?? null;
|
|
238
243
|
}
|
|
239
244
|
const gloss = getGloss(hint);
|
|
240
245
|
await services.updateSession(session.id, { hints_used: session.hints_used + 1, streak: 0 });
|
|
241
246
|
const updated = (await services.getById(session.id))!;
|
|
242
|
-
await sendOrEditView(plugin, services, message, updated, [
|
|
247
|
+
return (await sendOrEditView(plugin, services, message, updated, [
|
|
243
248
|
`💡 提示:可试 **${hint}**${gloss ? `(${gloss})` : ''}`,
|
|
244
249
|
'(使用提示会清零连击)',
|
|
245
|
-
]);
|
|
246
|
-
return null;
|
|
250
|
+
])) ?? null;
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
if (choiceId === 'skip') {
|
|
@@ -253,15 +257,15 @@ export async function handleChoice(
|
|
|
253
257
|
streak: 0,
|
|
254
258
|
});
|
|
255
259
|
const updated = (await services.getById(session.id))!;
|
|
256
|
-
await sendOrEditView(plugin, services, message, updated, [
|
|
257
|
-
|
|
260
|
+
return (await sendOrEditView(plugin, services, message, updated, [
|
|
261
|
+
'⏭️ 你选择跳过,机器人得一分。',
|
|
262
|
+
])) ?? null;
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
if (choiceId === 'quit') {
|
|
261
266
|
await services.updateSession(session.id, { status: 'lost', bot_score: session.bot_score + 1 });
|
|
262
267
|
const updated = (await services.getById(session.id))!;
|
|
263
|
-
await sendOrEditView(plugin, services, message, updated, ['🏳️ 你认输了。']);
|
|
264
|
-
return null;
|
|
268
|
+
return (await sendOrEditView(plugin, services, message, updated, ['🏳️ 你认输了。'])) ?? null;
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
return '未知操作。';
|
package/src/idiom-provider.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @see https://www.npmjs.com/package/chinese-idiom-chengyu
|
|
4
4
|
*/
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import { secureRandomItem } from '@zhin.js/game-kit';
|
|
6
7
|
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
|
|
@@ -101,12 +102,12 @@ export function pickBotIdiom(
|
|
|
101
102
|
): IdiomEntry | null {
|
|
102
103
|
const candidates = getValidNext(prevIdiom, mode, used);
|
|
103
104
|
if (!candidates.length) return null;
|
|
104
|
-
return
|
|
105
|
+
return secureRandomItem(candidates);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export function pickStarterIdiom(used: Set<string>): IdiomEntry {
|
|
108
109
|
const pool = buildFourCharPool().filter((w) => !used.has(w));
|
|
109
|
-
const text = pool
|
|
110
|
+
const text = pool.length > 0 ? secureRandomItem(pool) : '一心一意';
|
|
110
111
|
return { text, gloss: getGloss(text) };
|
|
111
112
|
}
|
|
112
113
|
|
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
|
-
registerChainHub(() => 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 { CHAIN_HELP } from './chain-command.js';
|
|
2
|
+
export { gameServicesToken, resolveGameServices } from './runtime-store.js';
|
|
3
|
+
export { createInMemoryChainDb, mountChainMemoryServices } 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 ChainDatabase, type SessionService } from './session-service.js';
|
|
5
|
+
|
|
6
|
+
const CHAIN_TABLES = ['idiom_chain_sessions'] as const;
|
|
7
|
+
|
|
8
|
+
/** In-memory idiom_chain_sessions for Plugin Runtime slice-2 (no DatabaseFeature). */
|
|
9
|
+
export function createInMemoryChainDb(): ChainDatabase {
|
|
10
|
+
return createInMemoryGameDb(CHAIN_TABLES) as unknown as ChainDatabase;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function mountChainMemoryServices(): SessionService {
|
|
14
|
+
const services = createServices(createInMemoryChainDb());
|
|
15
|
+
return services;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mountChainHostServices(host: DatabaseHost): SessionService {
|
|
19
|
+
defineHostTables(host);
|
|
20
|
+
const services = createServices(createHostGameDb(host, CHAIN_TABLES) as unknown as ChainDatabase);
|
|
21
|
+
return services;
|
|
22
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Models
|
|
1
|
+
import type { Models } from '@zhin.js/core';
|
|
2
2
|
import type { MatchMode } from './engine.js';
|
|
3
3
|
|
|
4
4
|
export type ChainSessionStatus = 'active' | 'won' | 'lost' | 'aborted';
|
|
5
5
|
|
|
6
|
-
declare module 'zhin.js' {
|
|
6
|
+
declare module '@zhin.js/core' {
|
|
7
7
|
interface Models {
|
|
8
8
|
idiom_chain_sessions: {
|
|
9
9
|
id: string;
|
|
@@ -35,8 +35,11 @@ declare module 'zhin.js' {
|
|
|
35
35
|
|
|
36
36
|
export type ChainSessionRow = Models['idiom_chain_sessions'];
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
export function defineHostTables(
|
|
40
|
+
db: { define: (name: string, definition: Record<string, unknown>) => void },
|
|
41
|
+
): void {
|
|
42
|
+
db.define('idiom_chain_sessions', {
|
|
40
43
|
id: { type: 'text', primary: true },
|
|
41
44
|
adapter: { type: 'text', nullable: false },
|
|
42
45
|
endpoint: { type: 'text', nullable: false },
|
|
@@ -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.idiom-chain.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,5 +1,5 @@
|
|
|
1
|
-
import type { Database,
|
|
2
|
-
import { channelKey, generateSessionId } from '@zhin.js/game-
|
|
1
|
+
import type { Database, Message, Models, RelatedModel } from '@zhin.js/core';
|
|
2
|
+
import { channelKey, generateSessionId, boardMessageMatches } from '@zhin.js/game-kit';
|
|
3
3
|
import type { MatchMode } from './engine.js';
|
|
4
4
|
import type { ChainSessionRow } from './models.js';
|
|
5
5
|
|
|
@@ -46,11 +46,9 @@ export class SessionService {
|
|
|
46
46
|
|
|
47
47
|
async getActiveByBoardMessageId(messageId: string): Promise<ChainSessionRow | null> {
|
|
48
48
|
if (!messageId) return null;
|
|
49
|
-
const rows = await getModel(this.db).findAll({
|
|
49
|
+
const rows = await getModel(this.db).findAll({});
|
|
50
50
|
for (const row of rows) {
|
|
51
|
-
|
|
52
|
-
if (!stored) continue;
|
|
53
|
-
if (stored === messageId || stored.endsWith(`:${messageId}`)) return row;
|
|
51
|
+
if (boardMessageMatches(row.board_message_id ?? '', messageId)) return row;
|
|
54
52
|
}
|
|
55
53
|
return null;
|
|
56
54
|
}
|
|
@@ -112,6 +110,3 @@ export function createServices(db: ChainDatabase): SessionService {
|
|
|
112
110
|
return new SessionService(db);
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
export function resolveGameDatabase(feature: DatabaseFeature): ChainDatabase {
|
|
116
|
-
return feature.db;
|
|
117
|
-
}
|
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 type { ChainSessionRow } from './models.js';
|
|
4
4
|
import { CHAIN_PREFIX, getGloss, idiomCount, modeLabel, promptLine } from './engine.js';
|
|
5
5
|
|
|
@@ -12,6 +12,7 @@ function sessionMode(session: ChainSessionRow): 'char' | 'pinyin' {
|
|
|
12
12
|
export function buildChainView(
|
|
13
13
|
session: ChainSessionRow,
|
|
14
14
|
eventLines: string[] = [],
|
|
15
|
+
channelType?: string,
|
|
15
16
|
): SendContent {
|
|
16
17
|
const terminal = session.status !== 'active';
|
|
17
18
|
const mode = sessionMode(session);
|
|
@@ -44,7 +45,7 @@ export function buildChainView(
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
const choices = terminal
|
|
47
|
-
? [{ id: 'restart', label: '🔄 再来一局', style: 'primary' as const }]
|
|
48
|
+
? [{ id: 'restart', label: '🔄 再来一局', style: 'primary' as const, keepEnabledWhenTerminal: true }]
|
|
48
49
|
: [
|
|
49
50
|
{ id: 'hint', label: '💡 提示', style: 'secondary' as const },
|
|
50
51
|
{ id: 'skip', label: '⏭️ 跳过', style: 'secondary' as const },
|
|
@@ -59,6 +60,8 @@ export function buildChainView(
|
|
|
59
60
|
terminal,
|
|
60
61
|
buttonsPerRow: 3,
|
|
61
62
|
fallbackHint: '回复成语,或 1提示 2跳过 3认输',
|
|
63
|
+
interactionProfile: terminal ? 'terminal' : 'gameplay',
|
|
64
|
+
channelType,
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chain-command.d.ts","sourceRoot":"","sources":["../src/chain-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,eAAO,MAAM,UAAU,QAWX,CAAC;AAEb,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B"}
|
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;AASrF,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"}
|