@zhin.js/plugin-game-hub 1.1.21 → 1.1.23
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 +13 -1
- package/lib/catalog.d.ts +29 -0
- package/lib/catalog.js +107 -0
- package/lib/catalog.js.map +1 -0
- package/lib/console.d.ts +17 -0
- package/lib/console.js +32 -0
- package/lib/console.js.map +1 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +40 -5
- package/pages/index/index.js +53 -0
- package/pages/index/index.ts +58 -0
- package/plugin.js +16 -2
- package/skills/game-hub/SKILL.md +20 -0
- package/skills/game-hub/tools/game_catalog/index.js +29 -0
- package/skills/game-hub/tools/game_catalog/index.ts +35 -0
- package/src/catalog.ts +137 -0
- package/src/console.ts +57 -0
- package/src/index.ts +14 -2
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
# @zhin.js/plugin-game-hub
|
|
2
2
|
|
|
3
|
-
Plugin Runtime
|
|
3
|
+
Plugin Runtime 游戏大厅:聚合展示当前 generation 已注册的小游戏插件(`GameIndex` 投影)、个人战绩与排行榜。
|
|
4
|
+
|
|
5
|
+
## 能力
|
|
6
|
+
|
|
7
|
+
- **聊天命令**:
|
|
8
|
+
- `games [action]`:列出已加载的小游戏插件及快捷开局提示。
|
|
9
|
+
- `战绩`:查询当前用户在各小游戏中的胜负与积分统计。
|
|
10
|
+
- `排行 [游戏名]`:查看指定小游戏的积分排行榜。
|
|
11
|
+
- **AI Skill 与 Tool**:
|
|
12
|
+
- `skills/game-hub/SKILL.md`:按需激活的游戏大厅技能。
|
|
13
|
+
- `skills/game-hub/tools/game_catalog/index.ts`(`game_catalog`,`readOnly: true`):查询已注册小游戏目录(ID、名称、别名、人数范围、简介与开局指令),支持按名称或别名过滤。
|
|
14
|
+
- **Console 页面**:
|
|
15
|
+
- `pages/index/index.ts`(权限 `game-hub:read`):通过 `pluginConsoleRpcToken`(`createGameHubConsoleRpc`)展示已注册游戏数量与游戏目录表。
|
package/lib/catalog.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type GameIndex, type RuntimeRegisteredGame } from '@zhin.js/game-kit';
|
|
2
|
+
export declare const gameIndexStore: import("zhin.js").PluginStore<GameIndex, unknown>;
|
|
3
|
+
export declare const gameIndexToken: import("zhin.js").PluginStore<GameIndex, unknown>;
|
|
4
|
+
export interface GameCatalogEntry {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly icon: string;
|
|
8
|
+
readonly aliases: readonly string[];
|
|
9
|
+
readonly minPlayers: number;
|
|
10
|
+
readonly maxPlayers: number;
|
|
11
|
+
readonly playerCountRange: string;
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly startCommand: string;
|
|
14
|
+
}
|
|
15
|
+
type RuntimeGameWithPlayers = RuntimeRegisteredGame & {
|
|
16
|
+
readonly minPlayers?: number;
|
|
17
|
+
readonly maxPlayers?: number;
|
|
18
|
+
};
|
|
19
|
+
type ContextWithIndexLookup = {
|
|
20
|
+
readonly project?: (feature: never) => unknown;
|
|
21
|
+
readonly use?: (token: never) => unknown;
|
|
22
|
+
};
|
|
23
|
+
export declare function resolveGameIndex(context?: ContextWithIndexLookup): GameIndex | undefined;
|
|
24
|
+
export declare function inferPlayerBounds(game: RuntimeGameWithPlayers): {
|
|
25
|
+
readonly minPlayers: number;
|
|
26
|
+
readonly maxPlayers: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function toGameCatalogEntry(game: RuntimeRegisteredGame): GameCatalogEntry;
|
|
29
|
+
export {};
|
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { definePluginStore } from 'zhin.js';
|
|
2
|
+
import { gameFeatureId, isGameIndex, } from '@zhin.js/game-kit';
|
|
3
|
+
export const gameIndexStore = definePluginStore('zhin.game.index', 'Game Hub registered game index');
|
|
4
|
+
export const gameIndexToken = gameIndexStore;
|
|
5
|
+
function isGameIndexLike(value) {
|
|
6
|
+
return isGameIndex(value)
|
|
7
|
+
|| (typeof value === 'object' && value !== null && typeof value.list === 'function');
|
|
8
|
+
}
|
|
9
|
+
export function resolveGameIndex(context) {
|
|
10
|
+
if (!context)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (typeof context.use === 'function') {
|
|
13
|
+
try {
|
|
14
|
+
const fromToken = context.use(gameIndexToken);
|
|
15
|
+
if (isGameIndexLike(fromToken))
|
|
16
|
+
return fromToken;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Fallback to feature projection when token is not bound.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (typeof context.project === 'function') {
|
|
23
|
+
try {
|
|
24
|
+
const projected = context.project(gameFeatureId);
|
|
25
|
+
if (isGameIndexLike(projected))
|
|
26
|
+
return projected;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Feature projection may not be available in all call contexts.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function isAsciiDigitCode(code) {
|
|
35
|
+
return code >= 48 && code <= 57;
|
|
36
|
+
}
|
|
37
|
+
function isAsciiDigitsOnly(value) {
|
|
38
|
+
if (value.length === 0 || value.length > 4)
|
|
39
|
+
return false;
|
|
40
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
41
|
+
if (!isAsciiDigitCode(value.charCodeAt(i)))
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
function parsePlayerRangeFromDescription(description) {
|
|
47
|
+
const text = description.slice(0, 200);
|
|
48
|
+
let searchFrom = 0;
|
|
49
|
+
while (searchFrom < text.length) {
|
|
50
|
+
const personIdx = text.indexOf('人', searchFrom);
|
|
51
|
+
if (personIdx <= 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
searchFrom = personIdx + 1;
|
|
54
|
+
const prefix = text.slice(0, personIdx).trimEnd();
|
|
55
|
+
for (const sep of ['-', '~', '至']) {
|
|
56
|
+
const sepIdx = prefix.lastIndexOf(sep);
|
|
57
|
+
if (sepIdx <= 0)
|
|
58
|
+
continue;
|
|
59
|
+
const rightStr = prefix.slice(sepIdx + sep.length).trim();
|
|
60
|
+
if (!isAsciiDigitsOnly(rightStr))
|
|
61
|
+
continue;
|
|
62
|
+
const leftPrefix = prefix.slice(0, sepIdx).trimEnd();
|
|
63
|
+
let leftStart = leftPrefix.length;
|
|
64
|
+
while (leftStart > 0 && isAsciiDigitCode(leftPrefix.charCodeAt(leftStart - 1))) {
|
|
65
|
+
leftStart -= 1;
|
|
66
|
+
}
|
|
67
|
+
const leftStr = leftPrefix.slice(leftStart);
|
|
68
|
+
if (!isAsciiDigitsOnly(leftStr))
|
|
69
|
+
continue;
|
|
70
|
+
return {
|
|
71
|
+
minPlayers: Number(leftStr),
|
|
72
|
+
maxPlayers: Number(rightStr),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
export function inferPlayerBounds(game) {
|
|
79
|
+
if (typeof game.minPlayers === 'number' && typeof game.maxPlayers === 'number') {
|
|
80
|
+
return { minPlayers: game.minPlayers, maxPlayers: game.maxPlayers };
|
|
81
|
+
}
|
|
82
|
+
const parsedRange = parsePlayerRangeFromDescription(game.description);
|
|
83
|
+
if (parsedRange) {
|
|
84
|
+
return parsedRange;
|
|
85
|
+
}
|
|
86
|
+
if (game.id === 'roulette')
|
|
87
|
+
return { minPlayers: 2, maxPlayers: 6 };
|
|
88
|
+
if (game.id === 'ttt')
|
|
89
|
+
return { minPlayers: 1, maxPlayers: 2 };
|
|
90
|
+
return { minPlayers: 1, maxPlayers: 1 };
|
|
91
|
+
}
|
|
92
|
+
export function toGameCatalogEntry(game) {
|
|
93
|
+
const { minPlayers, maxPlayers } = inferPlayerBounds(game);
|
|
94
|
+
const quickStart = game.quickStart ? ` ${game.quickStart}` : '';
|
|
95
|
+
return {
|
|
96
|
+
id: game.id,
|
|
97
|
+
name: game.title,
|
|
98
|
+
icon: game.icon,
|
|
99
|
+
aliases: game.aliases ?? [],
|
|
100
|
+
minPlayers,
|
|
101
|
+
maxPlayers,
|
|
102
|
+
playerCountRange: minPlayers === maxPlayers ? `${minPlayers}` : `${minPlayers}-${maxPlayers}`,
|
|
103
|
+
description: game.description,
|
|
104
|
+
startCommand: `${game.commandPrefix}${quickStart}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
|
package/lib/console.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type PluginConsoleRpc } from 'zhin.js';
|
|
2
|
+
import type { GameIndex } from '@zhin.js/game-kit';
|
|
3
|
+
export declare const GAME_HUB_CONSOLE_PAGE = "index";
|
|
4
|
+
export declare const GAME_HUB_READ_PERMISSION = "game-hub:read";
|
|
5
|
+
export interface GameHubConsoleGameRow {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly aliases: string;
|
|
9
|
+
readonly minPlayers: number;
|
|
10
|
+
readonly maxPlayers: number;
|
|
11
|
+
readonly description: string;
|
|
12
|
+
}
|
|
13
|
+
export interface GameHubConsoleStatus {
|
|
14
|
+
readonly gameCount: number;
|
|
15
|
+
readonly games: readonly GameHubConsoleGameRow[];
|
|
16
|
+
}
|
|
17
|
+
export declare function createGameHubConsoleRpc(fallbackIndex?: GameIndex): PluginConsoleRpc;
|
package/lib/console.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { definePluginConsoleRpc, } from 'zhin.js';
|
|
2
|
+
import { resolveGameIndex, toGameCatalogEntry, } from './catalog.js';
|
|
3
|
+
export const GAME_HUB_CONSOLE_PAGE = 'index';
|
|
4
|
+
export const GAME_HUB_READ_PERMISSION = 'game-hub:read';
|
|
5
|
+
export function createGameHubConsoleRpc(fallbackIndex) {
|
|
6
|
+
return definePluginConsoleRpc({
|
|
7
|
+
status: {
|
|
8
|
+
page: GAME_HUB_CONSOLE_PAGE,
|
|
9
|
+
requiredPermissions: [GAME_HUB_READ_PERMISSION],
|
|
10
|
+
handler(_input, call) {
|
|
11
|
+
const index = resolveGameIndex(call.runtime) ?? fallbackIndex;
|
|
12
|
+
const registered = index ? index.list() : [];
|
|
13
|
+
const games = registered.map((game) => {
|
|
14
|
+
const entry = toGameCatalogEntry(game);
|
|
15
|
+
return {
|
|
16
|
+
id: entry.id,
|
|
17
|
+
name: entry.name,
|
|
18
|
+
aliases: entry.aliases.join(', '),
|
|
19
|
+
minPlayers: entry.minPlayers,
|
|
20
|
+
maxPlayers: entry.maxPlayers,
|
|
21
|
+
description: entry.description,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
gameCount: games.length,
|
|
26
|
+
games,
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=console.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.js","sourceRoot":"","sources":["../src/console.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,GAGvB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAEtB,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAC;AAC7C,MAAM,CAAC,MAAM,wBAAwB,GAAG,eAAe,CAAC;AAgBxD,MAAM,UAAU,uBAAuB,CACrC,aAAyB;IAEzB,OAAO,sBAAsB,CAAC;QAC5B,MAAM,EAAE;YACN,IAAI,EAAE,qBAAqB;YAC3B,mBAAmB,EAAE,CAAC,wBAAwB,CAAC;YAC/C,OAAO,CAAC,MAAM,EAAE,IAA0B;gBACxC,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAgB,CAAC,IAAI,aAAa,CAAC;gBACvE,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;oBACpC,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;oBACvC,OAAO;wBACL,EAAE,EAAE,KAAK,CAAC,EAAE;wBACZ,IAAI,EAAE,KAAK,CAAC,IAAI;wBAChB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;wBACjC,UAAU,EAAE,KAAK,CAAC,UAAU;wBAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;wBAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;qBAC/B,CAAC;gBACJ,CAAC,CAAC,CAAC;gBACH,OAAO;oBACL,SAAS,EAAE,KAAK,CAAC,MAAM;oBACvB,KAAK;iBACN,CAAC;YACJ,CAAC;SACF;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export {};
|
|
1
|
+
export { gameIndexToken, inferPlayerBounds, resolveGameIndex, toGameCatalogEntry, type GameCatalogEntry, } from './catalog.js';
|
|
2
|
+
export { createGameHubConsoleRpc, GAME_HUB_CONSOLE_PAGE, GAME_HUB_READ_PERMISSION, type GameHubConsoleGameRow, type GameHubConsoleStatus, } from './console.js';
|
package/lib/index.js
CHANGED
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,GAEnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,GAGzB,MAAM,cAAc,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-game-hub",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.23",
|
|
4
4
|
"description": "Zhin.js 游戏大厅 — 列出已加载的游戏插件 (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -16,18 +16,25 @@
|
|
|
16
16
|
"plugin.js",
|
|
17
17
|
"schema.json",
|
|
18
18
|
"commands",
|
|
19
|
+
"pages",
|
|
20
|
+
"skills",
|
|
19
21
|
"src",
|
|
20
22
|
"lib",
|
|
21
23
|
"README.md"
|
|
22
24
|
],
|
|
23
25
|
"license": "MIT",
|
|
24
26
|
"dependencies": {
|
|
25
|
-
"@zhin.js/
|
|
27
|
+
"@zhin.js/console-contract": "1.1.4",
|
|
28
|
+
"@zhin.js/game-kit": "1.1.23",
|
|
29
|
+
"@zhin.js/page": "1.1.16",
|
|
30
|
+
"@zhin.js/skill": "1.1.15",
|
|
31
|
+
"@zhin.js/tool": "1.1.17"
|
|
26
32
|
},
|
|
27
33
|
"devDependencies": {
|
|
28
34
|
"typescript": "^6.0.3",
|
|
29
35
|
"vitest": "^4.1.10",
|
|
30
|
-
"
|
|
36
|
+
"zod": "^4.4.3",
|
|
37
|
+
"zhin.js": "1.1.23"
|
|
31
38
|
},
|
|
32
39
|
"repository": {
|
|
33
40
|
"type": "git",
|
|
@@ -55,17 +62,45 @@
|
|
|
55
62
|
{
|
|
56
63
|
"package": "@zhin.js/game-kit",
|
|
57
64
|
"api": "^1.0.0"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"package": "@zhin.js/tool",
|
|
68
|
+
"api": "^1.1.0"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"package": "@zhin.js/skill",
|
|
72
|
+
"api": "^1.0.0"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"package": "@zhin.js/page",
|
|
76
|
+
"api": "^1.0.0"
|
|
58
77
|
}
|
|
59
78
|
],
|
|
60
79
|
"plugins": []
|
|
61
80
|
},
|
|
62
81
|
"peerDependencies": {
|
|
63
|
-
"
|
|
64
|
-
"zhin.js": "^1.1.
|
|
82
|
+
"zod": "^4.0.0",
|
|
83
|
+
"@zhin.js/command": "^1.1.16",
|
|
84
|
+
"@zhin.js/page": "^1.1.16",
|
|
85
|
+
"@zhin.js/skill": "^1.1.15",
|
|
86
|
+
"@zhin.js/tool": "^1.1.17",
|
|
87
|
+
"zhin.js": "^1.1.23"
|
|
65
88
|
},
|
|
66
89
|
"peerDependenciesMeta": {
|
|
67
90
|
"@zhin.js/command": {
|
|
68
91
|
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"@zhin.js/page": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"@zhin.js/skill": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"@zhin.js/tool": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"zod": {
|
|
103
|
+
"optional": true
|
|
69
104
|
}
|
|
70
105
|
},
|
|
71
106
|
"scripts": {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { definePage, createDeclarativePage } from '@zhin.js/console-contract';
|
|
3
|
+
export const meta = definePage({
|
|
4
|
+
title: '游戏大厅',
|
|
5
|
+
titleI18n: { en: 'Game Hub' },
|
|
6
|
+
icon: 'Gamepad2',
|
|
7
|
+
order: 57,
|
|
8
|
+
requiredPermissions: ['game-hub:read'],
|
|
9
|
+
});
|
|
10
|
+
export default createDeclarativePage({
|
|
11
|
+
title: { zh: '游戏大厅', en: 'Game Hub' },
|
|
12
|
+
description: {
|
|
13
|
+
zh: '查看当前已加载的小游戏插件、别名、人数范围与玩法说明',
|
|
14
|
+
en: 'Browse registered mini-game plugins, aliases, player limits, and descriptions',
|
|
15
|
+
},
|
|
16
|
+
stats: [
|
|
17
|
+
{
|
|
18
|
+
key: 'gameCount',
|
|
19
|
+
label: { zh: '已注册游戏数', en: 'Registered Games' },
|
|
20
|
+
value: 'gameCount',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
sections: [
|
|
24
|
+
{
|
|
25
|
+
id: 'games',
|
|
26
|
+
title: { zh: '已加载游戏', en: 'Registered Games' },
|
|
27
|
+
emptyText: { zh: '暂无已注册的游戏插件', en: 'No game plugins registered' },
|
|
28
|
+
items: 'games',
|
|
29
|
+
columns: [
|
|
30
|
+
{
|
|
31
|
+
key: 'name',
|
|
32
|
+
label: { zh: '游戏名称', en: 'Name' },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'aliases',
|
|
36
|
+
label: { zh: '别名', en: 'Aliases' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'minPlayers',
|
|
40
|
+
label: { zh: '最少人数', en: 'Min Players' },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'maxPlayers',
|
|
44
|
+
label: { zh: '最多人数', en: 'Max Players' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'description',
|
|
48
|
+
label: { zh: '简介', en: 'Description' },
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { definePage, createDeclarativePage } from '@zhin.js/console-contract';
|
|
2
|
+
import type { GameHubConsoleGameRow, GameHubConsoleStatus } from '../../src/console.js';
|
|
3
|
+
|
|
4
|
+
export const meta = definePage({
|
|
5
|
+
title: '游戏大厅',
|
|
6
|
+
titleI18n: { en: 'Game Hub' },
|
|
7
|
+
icon: 'Gamepad2',
|
|
8
|
+
order: 57,
|
|
9
|
+
requiredPermissions: ['game-hub:read'],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default createDeclarativePage<GameHubConsoleStatus>({
|
|
13
|
+
title: { zh: '游戏大厅', en: 'Game Hub' },
|
|
14
|
+
description: {
|
|
15
|
+
zh: '查看当前已加载的小游戏插件、别名、人数范围与玩法说明',
|
|
16
|
+
en: 'Browse registered mini-game plugins, aliases, player limits, and descriptions',
|
|
17
|
+
},
|
|
18
|
+
stats: [
|
|
19
|
+
{
|
|
20
|
+
key: 'gameCount',
|
|
21
|
+
label: { zh: '已注册游戏数', en: 'Registered Games' },
|
|
22
|
+
value: 'gameCount',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
sections: [
|
|
26
|
+
{
|
|
27
|
+
id: 'games',
|
|
28
|
+
title: { zh: '已加载游戏', en: 'Registered Games' },
|
|
29
|
+
emptyText: { zh: '暂无已注册的游戏插件', en: 'No game plugins registered' },
|
|
30
|
+
items: 'games',
|
|
31
|
+
columns: [
|
|
32
|
+
{
|
|
33
|
+
key: 'name',
|
|
34
|
+
label: { zh: '游戏名称', en: 'Name' },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'aliases',
|
|
38
|
+
label: { zh: '别名', en: 'Aliases' },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'minPlayers',
|
|
42
|
+
label: { zh: '最少人数', en: 'Min Players' },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'maxPlayers',
|
|
46
|
+
label: { zh: '最多人数', en: 'Max Players' },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'description',
|
|
50
|
+
label: { zh: '简介', en: 'Description' },
|
|
51
|
+
},
|
|
52
|
+
] satisfies readonly {
|
|
53
|
+
readonly key: keyof GameHubConsoleGameRow & string;
|
|
54
|
+
readonly label: { readonly zh: string; readonly en: string };
|
|
55
|
+
}[],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
package/plugin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { definePlugin } from 'zhin.js';
|
|
2
|
+
import { definePlugin, definePluginCapabilityGraph, pluginConsoleRpcToken, } from 'zhin.js';
|
|
3
|
+
import { createGameHubConsoleRpc } from "./lib/console.js";
|
|
3
4
|
/**
|
|
4
5
|
* Plugin Runtime game hub — commands read the generation-owned GameIndex projection.
|
|
5
6
|
* Interactive hub menus are deferred; `/games` command shows help text.
|
|
@@ -9,5 +10,18 @@ export default definePlugin({
|
|
|
9
10
|
metadata: {
|
|
10
11
|
displayName: 'Game Hub',
|
|
11
12
|
},
|
|
12
|
-
|
|
13
|
+
capabilityGraph: definePluginCapabilityGraph({
|
|
14
|
+
provides: [pluginConsoleRpcToken],
|
|
15
|
+
features: {
|
|
16
|
+
commands: ['games', '战绩', '排行'],
|
|
17
|
+
skills: ['game-hub'],
|
|
18
|
+
tools: ['game_catalog'],
|
|
19
|
+
pages: ['index'],
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
setup() {
|
|
23
|
+
return {
|
|
24
|
+
consoleRpc: createGameHubConsoleRpc(),
|
|
25
|
+
};
|
|
26
|
+
},
|
|
13
27
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: game-hub
|
|
3
|
+
description: List registered mini-games in the Game Hub and explain rules/commands for starting or quitting a game.
|
|
4
|
+
keywords:
|
|
5
|
+
- "游戏"
|
|
6
|
+
- "小游戏"
|
|
7
|
+
- "大厅"
|
|
8
|
+
- "games"
|
|
9
|
+
- "game-hub"
|
|
10
|
+
- "play"
|
|
11
|
+
tags:
|
|
12
|
+
- "game"
|
|
13
|
+
- "entertainment"
|
|
14
|
+
tools:
|
|
15
|
+
- "game_catalog"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# game-hub
|
|
19
|
+
|
|
20
|
+
当用户询问有哪些小游戏可玩、如何开始或退出小游戏、或者查看游戏大厅说明时激活。使用 `game_catalog` 工具获取当前已加载的小游戏列表、别名、人数范围、简介与开局指令。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { resolveGameIndex, toGameCatalogEntry, } from "../../../../lib/catalog.js";
|
|
5
|
+
const inputSchema = z.object({
|
|
6
|
+
game: z.string().optional().describe('Optional game name or id to filter'),
|
|
7
|
+
});
|
|
8
|
+
export default defineAgentTool({
|
|
9
|
+
description: '查询游戏大厅中已注册的小游戏目录(名称、别名、人数范围、简介与开局指令)',
|
|
10
|
+
inputSchema,
|
|
11
|
+
readOnly: true,
|
|
12
|
+
requiresApproval: 'never',
|
|
13
|
+
tags: ['game', 'entertainment'],
|
|
14
|
+
keywords: ['游戏', '小游戏', '大厅', 'games', 'game-hub'],
|
|
15
|
+
execute({ game }, context) {
|
|
16
|
+
const index = resolveGameIndex(context);
|
|
17
|
+
const allGames = index ? index.list().map(toGameCatalogEntry) : [];
|
|
18
|
+
const query = game?.trim().toLowerCase();
|
|
19
|
+
const games = query
|
|
20
|
+
? allGames.filter((entry) => entry.id.toLowerCase().includes(query)
|
|
21
|
+
|| entry.name.toLowerCase().includes(query)
|
|
22
|
+
|| entry.aliases.some((alias) => alias.toLowerCase().includes(query)))
|
|
23
|
+
: allGames;
|
|
24
|
+
return {
|
|
25
|
+
count: games.length,
|
|
26
|
+
games,
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/tool';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
resolveGameIndex,
|
|
5
|
+
toGameCatalogEntry,
|
|
6
|
+
type GameCatalogEntry,
|
|
7
|
+
} from '../../../../src/catalog.js';
|
|
8
|
+
|
|
9
|
+
const inputSchema = z.object({
|
|
10
|
+
game: z.string().optional().describe('Optional game name or id to filter'),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default defineAgentTool<{ game?: string }, { count: number; games: GameCatalogEntry[] }>({
|
|
14
|
+
description: '查询游戏大厅中已注册的小游戏目录(名称、别名、人数范围、简介与开局指令)',
|
|
15
|
+
inputSchema,
|
|
16
|
+
readOnly: true,
|
|
17
|
+
requiresApproval: 'never',
|
|
18
|
+
tags: ['game', 'entertainment'],
|
|
19
|
+
keywords: ['游戏', '小游戏', '大厅', 'games', 'game-hub'],
|
|
20
|
+
execute({ game }, context) {
|
|
21
|
+
const index = resolveGameIndex(context);
|
|
22
|
+
const allGames = index ? index.list().map(toGameCatalogEntry) : [];
|
|
23
|
+
const query = game?.trim().toLowerCase();
|
|
24
|
+
const games = query
|
|
25
|
+
? allGames.filter((entry) =>
|
|
26
|
+
entry.id.toLowerCase().includes(query)
|
|
27
|
+
|| entry.name.toLowerCase().includes(query)
|
|
28
|
+
|| entry.aliases.some((alias) => alias.toLowerCase().includes(query)))
|
|
29
|
+
: allGames;
|
|
30
|
+
return {
|
|
31
|
+
count: games.length,
|
|
32
|
+
games,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { definePluginStore } from 'zhin.js';
|
|
2
|
+
import {
|
|
3
|
+
gameFeatureId,
|
|
4
|
+
isGameIndex,
|
|
5
|
+
type GameIndex,
|
|
6
|
+
type RuntimeRegisteredGame,
|
|
7
|
+
} from '@zhin.js/game-kit';
|
|
8
|
+
|
|
9
|
+
export const gameIndexStore = definePluginStore<GameIndex>(
|
|
10
|
+
'zhin.game.index',
|
|
11
|
+
'Game Hub registered game index',
|
|
12
|
+
);
|
|
13
|
+
export const gameIndexToken = gameIndexStore;
|
|
14
|
+
|
|
15
|
+
export interface GameCatalogEntry {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly icon: string;
|
|
19
|
+
readonly aliases: readonly string[];
|
|
20
|
+
readonly minPlayers: number;
|
|
21
|
+
readonly maxPlayers: number;
|
|
22
|
+
readonly playerCountRange: string;
|
|
23
|
+
readonly description: string;
|
|
24
|
+
readonly startCommand: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type RuntimeGameWithPlayers = RuntimeRegisteredGame & {
|
|
28
|
+
readonly minPlayers?: number;
|
|
29
|
+
readonly maxPlayers?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ContextWithIndexLookup = {
|
|
33
|
+
readonly project?: (feature: never) => unknown;
|
|
34
|
+
readonly use?: (token: never) => unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function isGameIndexLike(value: unknown): value is GameIndex {
|
|
38
|
+
return isGameIndex(value)
|
|
39
|
+
|| (typeof value === 'object' && value !== null && typeof (value as GameIndex).list === 'function');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveGameIndex(context?: ContextWithIndexLookup): GameIndex | undefined {
|
|
43
|
+
if (!context) return undefined;
|
|
44
|
+
if (typeof context.use === 'function') {
|
|
45
|
+
try {
|
|
46
|
+
const fromToken = (context.use as (token: unknown) => unknown)(gameIndexToken);
|
|
47
|
+
if (isGameIndexLike(fromToken)) return fromToken;
|
|
48
|
+
} catch {
|
|
49
|
+
// Fallback to feature projection when token is not bound.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (typeof context.project === 'function') {
|
|
53
|
+
try {
|
|
54
|
+
const projected = (context.project as (feature: unknown) => unknown)(gameFeatureId);
|
|
55
|
+
if (isGameIndexLike(projected)) return projected;
|
|
56
|
+
} catch {
|
|
57
|
+
// Feature projection may not be available in all call contexts.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isAsciiDigitCode(code: number): boolean {
|
|
64
|
+
return code >= 48 && code <= 57;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isAsciiDigitsOnly(value: string): boolean {
|
|
68
|
+
if (value.length === 0 || value.length > 4) return false;
|
|
69
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
70
|
+
if (!isAsciiDigitCode(value.charCodeAt(i))) return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parsePlayerRangeFromDescription(description: string): {
|
|
76
|
+
readonly minPlayers: number;
|
|
77
|
+
readonly maxPlayers: number;
|
|
78
|
+
} | undefined {
|
|
79
|
+
const text = description.slice(0, 200);
|
|
80
|
+
let searchFrom = 0;
|
|
81
|
+
while (searchFrom < text.length) {
|
|
82
|
+
const personIdx = text.indexOf('人', searchFrom);
|
|
83
|
+
if (personIdx <= 0) return undefined;
|
|
84
|
+
searchFrom = personIdx + 1;
|
|
85
|
+
const prefix = text.slice(0, personIdx).trimEnd();
|
|
86
|
+
for (const sep of ['-', '~', '至']) {
|
|
87
|
+
const sepIdx = prefix.lastIndexOf(sep);
|
|
88
|
+
if (sepIdx <= 0) continue;
|
|
89
|
+
const rightStr = prefix.slice(sepIdx + sep.length).trim();
|
|
90
|
+
if (!isAsciiDigitsOnly(rightStr)) continue;
|
|
91
|
+
const leftPrefix = prefix.slice(0, sepIdx).trimEnd();
|
|
92
|
+
let leftStart = leftPrefix.length;
|
|
93
|
+
while (leftStart > 0 && isAsciiDigitCode(leftPrefix.charCodeAt(leftStart - 1))) {
|
|
94
|
+
leftStart -= 1;
|
|
95
|
+
}
|
|
96
|
+
const leftStr = leftPrefix.slice(leftStart);
|
|
97
|
+
if (!isAsciiDigitsOnly(leftStr)) continue;
|
|
98
|
+
return {
|
|
99
|
+
minPlayers: Number(leftStr),
|
|
100
|
+
maxPlayers: Number(rightStr),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function inferPlayerBounds(game: RuntimeGameWithPlayers): {
|
|
108
|
+
readonly minPlayers: number;
|
|
109
|
+
readonly maxPlayers: number;
|
|
110
|
+
} {
|
|
111
|
+
if (typeof game.minPlayers === 'number' && typeof game.maxPlayers === 'number') {
|
|
112
|
+
return { minPlayers: game.minPlayers, maxPlayers: game.maxPlayers };
|
|
113
|
+
}
|
|
114
|
+
const parsedRange = parsePlayerRangeFromDescription(game.description);
|
|
115
|
+
if (parsedRange) {
|
|
116
|
+
return parsedRange;
|
|
117
|
+
}
|
|
118
|
+
if (game.id === 'roulette') return { minPlayers: 2, maxPlayers: 6 };
|
|
119
|
+
if (game.id === 'ttt') return { minPlayers: 1, maxPlayers: 2 };
|
|
120
|
+
return { minPlayers: 1, maxPlayers: 1 };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function toGameCatalogEntry(game: RuntimeRegisteredGame): GameCatalogEntry {
|
|
124
|
+
const { minPlayers, maxPlayers } = inferPlayerBounds(game);
|
|
125
|
+
const quickStart = game.quickStart ? ` ${game.quickStart}` : '';
|
|
126
|
+
return {
|
|
127
|
+
id: game.id,
|
|
128
|
+
name: game.title,
|
|
129
|
+
icon: game.icon,
|
|
130
|
+
aliases: game.aliases ?? [],
|
|
131
|
+
minPlayers,
|
|
132
|
+
maxPlayers,
|
|
133
|
+
playerCountRange: minPlayers === maxPlayers ? `${minPlayers}` : `${minPlayers}-${maxPlayers}`,
|
|
134
|
+
description: game.description,
|
|
135
|
+
startCommand: `${game.commandPrefix}${quickStart}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
package/src/console.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
definePluginConsoleRpc,
|
|
3
|
+
type PluginConsoleRpc,
|
|
4
|
+
type PluginConsoleRpcCall,
|
|
5
|
+
} from 'zhin.js';
|
|
6
|
+
import type { GameIndex } from '@zhin.js/game-kit';
|
|
7
|
+
import {
|
|
8
|
+
resolveGameIndex,
|
|
9
|
+
toGameCatalogEntry,
|
|
10
|
+
} from './catalog.js';
|
|
11
|
+
|
|
12
|
+
export const GAME_HUB_CONSOLE_PAGE = 'index';
|
|
13
|
+
export const GAME_HUB_READ_PERMISSION = 'game-hub:read';
|
|
14
|
+
|
|
15
|
+
export interface GameHubConsoleGameRow {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly aliases: string;
|
|
19
|
+
readonly minPlayers: number;
|
|
20
|
+
readonly maxPlayers: number;
|
|
21
|
+
readonly description: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GameHubConsoleStatus {
|
|
25
|
+
readonly gameCount: number;
|
|
26
|
+
readonly games: readonly GameHubConsoleGameRow[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createGameHubConsoleRpc(
|
|
30
|
+
fallbackIndex?: GameIndex,
|
|
31
|
+
): PluginConsoleRpc {
|
|
32
|
+
return definePluginConsoleRpc({
|
|
33
|
+
status: {
|
|
34
|
+
page: GAME_HUB_CONSOLE_PAGE,
|
|
35
|
+
requiredPermissions: [GAME_HUB_READ_PERMISSION],
|
|
36
|
+
handler(_input, call: PluginConsoleRpcCall) {
|
|
37
|
+
const index = resolveGameIndex(call.runtime as never) ?? fallbackIndex;
|
|
38
|
+
const registered = index ? index.list() : [];
|
|
39
|
+
const games = registered.map((game) => {
|
|
40
|
+
const entry = toGameCatalogEntry(game);
|
|
41
|
+
return {
|
|
42
|
+
id: entry.id,
|
|
43
|
+
name: entry.name,
|
|
44
|
+
aliases: entry.aliases.join(', '),
|
|
45
|
+
minPlayers: entry.minPlayers,
|
|
46
|
+
maxPlayers: entry.maxPlayers,
|
|
47
|
+
description: entry.description,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
gameCount: games.length,
|
|
52
|
+
games,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
gameIndexToken,
|
|
3
|
+
inferPlayerBounds,
|
|
4
|
+
resolveGameIndex,
|
|
5
|
+
toGameCatalogEntry,
|
|
6
|
+
type GameCatalogEntry,
|
|
7
|
+
} from './catalog.js';
|
|
8
|
+
export {
|
|
9
|
+
createGameHubConsoleRpc,
|
|
10
|
+
GAME_HUB_CONSOLE_PAGE,
|
|
11
|
+
GAME_HUB_READ_PERMISSION,
|
|
12
|
+
type GameHubConsoleGameRow,
|
|
13
|
+
type GameHubConsoleStatus,
|
|
14
|
+
} from './console.js';
|