@dsh-plugin/dsh-synapse 0.1.0-dev.41044228975 → 0.1.0
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/LICENSE +661 -661
- package/README.md +0 -14
- package/package.json +1 -1
- package/vendor/gugle-chat-bot/LICENSE +661 -661
- package/vendor/gugle-chat-bot/package.json +57 -57
- package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
- package/vendor/gugle-chat-bot/src/api.json +4284 -4284
- package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
- package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
- package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
- package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
- package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
- package/vendor/gugle-chat-bot/src/custom.ts +548 -548
- package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
- package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
- package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
- package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
- package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
- package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
- package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
- package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
- package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
- package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
- package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
- package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
- package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
- package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
- package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
- package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
- package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
- package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
- package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
- package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
- package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
- package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
- package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
- package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
- package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
- package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
- package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
- package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
- package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
- package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
- package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
- package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
- package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
- package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
- package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
- package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
- package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
- package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
- package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
- package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
- package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
- package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
- package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
- package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
- package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
- package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
- package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
- package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
- package/vendor/gugle-chat-bot/src/index.ts +695 -695
- package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
- package/vendor/gugle-chat-bot/src/result.json +359 -359
- package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
- package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
- package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
- package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
- package/vendor/gugle-chat-bot/tsconfig.json +46 -46
- package/scripts/dump-npm-latest.mjs +0 -30
|
@@ -1,406 +1,406 @@
|
|
|
1
|
-
import {
|
|
2
|
-
GroupAdminApi,
|
|
3
|
-
ProcessedSkillOutput,
|
|
4
|
-
RouteInfo,
|
|
5
|
-
Skill,
|
|
6
|
-
SkillExecuteContext,
|
|
7
|
-
SkillFeedback,
|
|
8
|
-
SkillResult
|
|
9
|
-
} from '@/features/hermes/types';
|
|
10
|
-
import type { QQBot } from '@/index';
|
|
11
|
-
import { Github } from '@/features/github';
|
|
12
|
-
import { Bili } from '@/features/bili';
|
|
13
|
-
import { MinecraftAPI } from '@/features/minecraft';
|
|
14
|
-
import { ModrinthAPI } from '@/features/modrinth';
|
|
15
|
-
import { isGithubEnabledGroup } from '@/config/features';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 技能管理器(移植自 qq-hermes-bridge src/skills.ts,MIT 协议,原作者 Amorter)。
|
|
19
|
-
* 定义 AI 可调用的技能,生成技能提示词,解析并执行 AI 输出中的技能标签。
|
|
20
|
-
* 管理技能仅限有效管理员(hermes admins ∪ management operators)使用;
|
|
21
|
-
* 卡片类公共技能(查Issue / B站视频)所有人可用,产出图片由调用方逐张发送。
|
|
22
|
-
*/
|
|
23
|
-
export class SkillManager {
|
|
24
|
-
/** 技能标签匹配正则:[SKILL:名称 参数...] */
|
|
25
|
-
private static readonly SKILL_TAG_RE = /\[SKILL:([^\]]+)\]/g;
|
|
26
|
-
/** 已注册的技能列表 */
|
|
27
|
-
private readonly skills: Skill[];
|
|
28
|
-
/** 技能名 → 技能定义的快速索引 */
|
|
29
|
-
private readonly skillIndex: Map<string, Skill>;
|
|
30
|
-
|
|
31
|
-
public constructor() {
|
|
32
|
-
this.skills = [
|
|
33
|
-
{
|
|
34
|
-
name: '禁言',
|
|
35
|
-
usage: '禁言 <QQ号> <时长(分钟)>',
|
|
36
|
-
description: '禁言指定群成员,最长 30 天(43200 分钟)',
|
|
37
|
-
adminOnly: true,
|
|
38
|
-
execute: this.executeMute.bind(this)
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: '解除禁言',
|
|
42
|
-
usage: '解除禁言 <QQ号>',
|
|
43
|
-
description: '解除指定成员的禁言',
|
|
44
|
-
adminOnly: true,
|
|
45
|
-
execute: this.executeUnmute.bind(this)
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: '踢出',
|
|
49
|
-
usage: '踢出 <QQ号>',
|
|
50
|
-
description: '将指定成员踢出群聊',
|
|
51
|
-
adminOnly: true,
|
|
52
|
-
execute: this.executeKick.bind(this)
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: '全员禁言',
|
|
56
|
-
usage: '全员禁言 <开/关>',
|
|
57
|
-
description: '开启或关闭全员禁言',
|
|
58
|
-
adminOnly: true,
|
|
59
|
-
execute: this.executeWholeBan.bind(this)
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
name: '查Issue',
|
|
63
|
-
usage: '查Issue <owner/repo#编号 或 #编号>',
|
|
64
|
-
description: '查询 GitHub Issue/PR 并生成信息卡片(PR 自动识别;省略仓库时默认 Anvil-Dev/AnvilCraft;一条最多 3 个编号)',
|
|
65
|
-
adminOnly: false,
|
|
66
|
-
execute: this.executeIssueCard.bind(this)
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'B站视频',
|
|
70
|
-
usage: 'B站视频 <BV号/av号/b23.tv短链>',
|
|
71
|
-
description: '获取 B 站视频信息并生成卡片(标题/UP主/时长/播放点赞评论数)',
|
|
72
|
-
adminOnly: false,
|
|
73
|
-
execute: this.executeBiliCard.bind(this)
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: 'MC版本',
|
|
77
|
-
usage: 'MC版本',
|
|
78
|
-
description: '查询 Minecraft 最新正式版与快照版',
|
|
79
|
-
adminOnly: false,
|
|
80
|
-
feedback: true,
|
|
81
|
-
execute: this.executeMcVersion.bind(this)
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'MC服务器',
|
|
85
|
-
usage: 'MC服务器 <ip> [端口]',
|
|
86
|
-
description: '查询 Minecraft Java 服务器状态(MOTD/版本/在线玩家)',
|
|
87
|
-
adminOnly: false,
|
|
88
|
-
feedback: true,
|
|
89
|
-
execute: this.executeMcServer.bind(this)
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: 'MC维基',
|
|
93
|
-
usage: 'MC维基 <关键词>',
|
|
94
|
-
description: '搜索中文 Minecraft 维基,返回条目摘要与链接',
|
|
95
|
-
adminOnly: false,
|
|
96
|
-
feedback: true,
|
|
97
|
-
execute: this.executeMcWiki.bind(this)
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'Modrinth版本',
|
|
101
|
-
usage: 'Modrinth版本 <slug>',
|
|
102
|
-
description: '查询 Modrinth 模组最新版本与下载直链(slug 如 create-aeronautics)',
|
|
103
|
-
adminOnly: false,
|
|
104
|
-
feedback: true,
|
|
105
|
-
execute: this.executeModrinthVersion.bind(this)
|
|
106
|
-
}
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
this.skillIndex = new Map(this.skills.map(skill => [skill.name, skill]));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 构建技能列表提示词,注入到系统提示词中。
|
|
114
|
-
* 按「公共技能」和「管理技能(仅管理员)」分组展示。
|
|
115
|
-
*/
|
|
116
|
-
public buildPrompt(): string {
|
|
117
|
-
if (this.skills.length === 0) return '';
|
|
118
|
-
|
|
119
|
-
const adminSkills = this.skills.filter(skill => skill.adminOnly);
|
|
120
|
-
const publicSkills = this.skills.filter(skill => !skill.adminOnly && !skill.feedback);
|
|
121
|
-
const querySkills = this.skills.filter(skill => !skill.adminOnly && skill.feedback);
|
|
122
|
-
|
|
123
|
-
const lines: string[] = ['## 可用技能'];
|
|
124
|
-
|
|
125
|
-
if (publicSkills.length > 0) {
|
|
126
|
-
lines.push('');
|
|
127
|
-
for (const skill of publicSkills) {
|
|
128
|
-
lines.push(`- \`${skill.usage}\` — ${skill.description}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (querySkills.length > 0) {
|
|
133
|
-
lines.push('');
|
|
134
|
-
lines.push('### 查询技能(调用后结果会返回给你,请整理成口语化回答后再回复,不要复述原始数据)');
|
|
135
|
-
for (const skill of querySkills) {
|
|
136
|
-
lines.push(`- \`${skill.usage}\` — ${skill.description}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (adminSkills.length > 0) {
|
|
141
|
-
lines.push('');
|
|
142
|
-
lines.push('### 管理技能(仅管理员可用)');
|
|
143
|
-
for (const skill of adminSkills) {
|
|
144
|
-
lines.push(`- \`${skill.usage}\` — ${skill.description}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
lines.push('');
|
|
149
|
-
lines.push('调用格式:在回复中插入 `[SKILL:技能名 参数...]`,标签会在发送前被处理并移除。');
|
|
150
|
-
lines.push('注意:参数中的 QQ 号使用纯数字格式,多个技能可在一段话中同时调用。');
|
|
151
|
-
lines.push('卡片类技能(查Issue / B站视频)会以图片形式随回复一起发出,无需在文本中复述卡片内容。');
|
|
152
|
-
lines.push('查询类技能调用后请等待数据返回,不要假装已经知道结果。');
|
|
153
|
-
|
|
154
|
-
return lines.join('\n');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* 解析并执行 AI 输出中的 [SKILL:...] 标签。
|
|
159
|
-
* 全部标签执行完毕后从文本中移除,并在末尾附加执行摘要。
|
|
160
|
-
*
|
|
161
|
-
* @param output AI 生成的输出文本
|
|
162
|
-
* @param route 消息来源路由
|
|
163
|
-
* @param deps 依赖项(QQBot、群管理 API 门面和管理员检查函数)
|
|
164
|
-
* @returns 清理后的文本(含执行摘要)与全部技能产出的图片(base64)
|
|
165
|
-
*/
|
|
166
|
-
public async processTags(
|
|
167
|
-
output: string,
|
|
168
|
-
route: RouteInfo,
|
|
169
|
-
deps: { bot: QQBot; api: GroupAdminApi; isAdmin: (userId: string) => boolean }
|
|
170
|
-
): Promise<ProcessedSkillOutput> {
|
|
171
|
-
const { bot, api, isAdmin } = deps;
|
|
172
|
-
|
|
173
|
-
// 收集所有技能标签
|
|
174
|
-
const tags: Array<{ raw: string; content: string }> = [];
|
|
175
|
-
let match: RegExpExecArray | null;
|
|
176
|
-
SkillManager.SKILL_TAG_RE.lastIndex = 0;
|
|
177
|
-
while ((match = SkillManager.SKILL_TAG_RE.exec(output)) !== null) {
|
|
178
|
-
tags.push({ raw: match[0], content: match[1]! });
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (tags.length === 0) return { text: output, images: [], feedback: [] };
|
|
182
|
-
|
|
183
|
-
// 逐个解析并执行
|
|
184
|
-
const results: SkillResult[] = [];
|
|
185
|
-
const feedback: SkillFeedback[] = [];
|
|
186
|
-
for (const tag of tags) {
|
|
187
|
-
const parts = tag.content.trim().split(/\s+/);
|
|
188
|
-
const skillName = parts[0]!;
|
|
189
|
-
const args = parts.slice(1);
|
|
190
|
-
|
|
191
|
-
const skill = this.skillIndex.get(skillName);
|
|
192
|
-
if (!skill) {
|
|
193
|
-
results.push({ ok: false, skill: skillName, error: `未知技能: ${skillName}` });
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 管理员权限检查
|
|
198
|
-
if (skill.adminOnly && !isAdmin(route.userId)) {
|
|
199
|
-
results.push({ ok: false, skill: skillName, error: '仅管理员可用' });
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
const execution = await skill.execute({ bot, api, route, args });
|
|
205
|
-
const data = typeof execution === 'string' ? execution : execution.message;
|
|
206
|
-
if (skill.feedback) {
|
|
207
|
-
// 查询类:数据回喂给 AI 整理回答,不附加摘要
|
|
208
|
-
feedback.push({ skill: skillName, data });
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (typeof execution === 'string') {
|
|
212
|
-
results.push({ ok: true, skill: skillName, message: execution });
|
|
213
|
-
} else {
|
|
214
|
-
results.push({
|
|
215
|
-
ok: true,
|
|
216
|
-
skill: skillName,
|
|
217
|
-
message: execution.message,
|
|
218
|
-
...(execution.images?.length ? { images: execution.images } : {})
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
} catch (error) {
|
|
222
|
-
// 查询类失败同样回喂(让 AI 告知用户没查到),动作类记入错误摘要
|
|
223
|
-
if (skill.feedback) {
|
|
224
|
-
feedback.push({ skill: skillName, data: `执行失败:${(error as Error).message}` });
|
|
225
|
-
} else {
|
|
226
|
-
results.push({ ok: false, skill: skillName, error: (error as Error).message });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// 从文本中移除所有技能标签
|
|
232
|
-
let cleaned = output;
|
|
233
|
-
for (const tag of tags) {
|
|
234
|
-
cleaned = cleaned.replace(tag.raw, '');
|
|
235
|
-
}
|
|
236
|
-
cleaned = cleaned.replace(/ {2,}/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
237
|
-
|
|
238
|
-
// 附加执行摘要(整体 trim:整条消息只有技能标签时避免摘要前出现空行——对上游的微调)
|
|
239
|
-
const summaryLines: string[] = [];
|
|
240
|
-
const images: string[] = [];
|
|
241
|
-
for (const result of results) {
|
|
242
|
-
if (result.ok) {
|
|
243
|
-
summaryLines.push(`✅ ${result.message}`);
|
|
244
|
-
if (result.images) images.push(...result.images);
|
|
245
|
-
} else {
|
|
246
|
-
summaryLines.push(`❌ ${result.skill}: ${result.error}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (summaryLines.length > 0) {
|
|
250
|
-
cleaned = cleaned + '\n\n' + summaryLines.join('\n');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return { text: cleaned.trim(), images, feedback };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** 从参数中提取纯数字 QQ 号 */
|
|
257
|
-
private extractQq(arg: string | undefined): string | null {
|
|
258
|
-
if (!arg) return null;
|
|
259
|
-
const match = String(arg).match(/\((\d+)\)$/);
|
|
260
|
-
return match ? match[1]! : String(arg).replace(/\D/g, '') || null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** 解析正整数 */
|
|
264
|
-
private parsePositiveInt(text: string | undefined, max = Infinity): number | null {
|
|
265
|
-
const value = Number.parseInt(text ?? '', 10);
|
|
266
|
-
if (!Number.isFinite(value) || value <= 0) return null;
|
|
267
|
-
return Math.min(value, max);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** 执行禁言技能 */
|
|
271
|
-
private async executeMute({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
272
|
-
const qq = this.extractQq(args[0]);
|
|
273
|
-
if (!qq) throw new Error('缺少 QQ 号,格式: 禁言 <QQ号> <时长(分钟)>');
|
|
274
|
-
const minutes = this.parsePositiveInt(args[1], 43200);
|
|
275
|
-
if (!minutes) throw new Error('时长无效,格式: 禁言 <QQ号> <时长(分钟)>');
|
|
276
|
-
await api.setGroupBan(route.groupId!, qq, minutes * 60);
|
|
277
|
-
return `已禁言 ${qq} ${minutes} 分钟`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** 执行解除禁言技能 */
|
|
281
|
-
private async executeUnmute({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
282
|
-
const qq = this.extractQq(args[0]);
|
|
283
|
-
if (!qq) throw new Error('缺少 QQ 号,格式: 解除禁言 <QQ号>');
|
|
284
|
-
await api.setGroupBan(route.groupId!, qq, 0);
|
|
285
|
-
return `已解除 ${qq} 的禁言`;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** 执行踢出技能 */
|
|
289
|
-
private async executeKick({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
290
|
-
const qq = this.extractQq(args[0]);
|
|
291
|
-
if (!qq) throw new Error('缺少 QQ 号,格式: 踢出 <QQ号>');
|
|
292
|
-
await api.setGroupKick(route.groupId!, qq);
|
|
293
|
-
return `已将 ${qq} 踢出群聊`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** 执行全员禁言技能 */
|
|
297
|
-
private async executeWholeBan({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
298
|
-
const arg = String(args[0] || '').trim();
|
|
299
|
-
if (['开', '开启', '启用', 'on', 'true', '1'].includes(arg)) {
|
|
300
|
-
await api.setGroupWholeBan(route.groupId!, true);
|
|
301
|
-
return '已开启全员禁言';
|
|
302
|
-
}
|
|
303
|
-
if (['关', '关闭', '禁用', 'off', 'false', '0'].includes(arg)) {
|
|
304
|
-
await api.setGroupWholeBan(route.groupId!, false);
|
|
305
|
-
return '已关闭全员禁言';
|
|
306
|
-
}
|
|
307
|
-
throw new Error('参数无效,请使用 开 或 关');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ── 卡片类公共技能(复用 github / bili 功能的渲染管线) ──
|
|
311
|
-
|
|
312
|
-
/** 卡片技能一次最多生成的图片数(防刷屏) */
|
|
313
|
-
private static readonly MAX_CARD_IMAGES = 3;
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* 查Issue:解析编号引用并复用 Github.renderReference 渲染卡片(PR 自动识别)。
|
|
317
|
-
* 群/频道路由要求该群启用 github 功能;仓库允许列表判定在 renderReference 内完成。
|
|
318
|
-
*/
|
|
319
|
-
private async executeIssueCard({ bot, route, args }: SkillExecuteContext): Promise<{ message: string; images?: string[] }> {
|
|
320
|
-
const input = args.join(' ').trim();
|
|
321
|
-
if (!input) throw new Error('缺少编号,格式: 查Issue <owner/repo#编号 或 #编号>');
|
|
322
|
-
if (route.type !== 'user' && route.groupId && !isGithubEnabledGroup(Number(route.groupId))) {
|
|
323
|
-
throw new Error('本群未启用 GitHub 功能');
|
|
324
|
-
}
|
|
325
|
-
const references = Github.parseReferences(input).slice(0, SkillManager.MAX_CARD_IMAGES);
|
|
326
|
-
if (references.length === 0) {
|
|
327
|
-
throw new Error('未识别到 Issue/PR 编号,格式: 查Issue <owner/repo#编号 或 #编号>');
|
|
328
|
-
}
|
|
329
|
-
const images: string[] = [];
|
|
330
|
-
const notes: string[] = [];
|
|
331
|
-
for (const reference of references) {
|
|
332
|
-
const rendered = await Github.renderReference(bot, reference);
|
|
333
|
-
// Message 联合类型中部分成员(如 DiceMessage)无 data 字段,整体断言后取值
|
|
334
|
-
const data = (rendered as { data?: { file?: string; text?: string } }).data ?? {};
|
|
335
|
-
if (rendered.type === 'image') {
|
|
336
|
-
const file = data.file ?? '';
|
|
337
|
-
const comma = file.indexOf(',');
|
|
338
|
-
const base64 = (comma >= 0 ? file.slice(comma + 1) : file).trim();
|
|
339
|
-
images.push(base64);
|
|
340
|
-
notes.push(`${reference.repository}#${reference.number}`);
|
|
341
|
-
} else {
|
|
342
|
-
// 渲染失败/仓库不允许:尚无成功卡片时整体失败;已有成功卡片时并入摘要说明
|
|
343
|
-
const reason = data.text ?? '渲染失败';
|
|
344
|
-
if (images.length === 0) throw new Error(reason);
|
|
345
|
-
notes.push(`${reference.repository}#${reference.number}(${reason})`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return { message: `已生成卡片:${notes.join('、')}`, images };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** B站视频:BV号/av号/b23.tv 短链 → Bili.renderVideoCard 渲染信息卡片 */
|
|
352
|
-
private async executeBiliCard({ bot, args }: SkillExecuteContext): Promise<{ message: string; images: string[] }> {
|
|
353
|
-
const input = args.join(' ').trim();
|
|
354
|
-
if (!input) throw new Error('缺少视频号,格式: B站视频 <BV号/av号/b23.tv短链>');
|
|
355
|
-
const base64 = await Bili.renderVideoCard(bot, input);
|
|
356
|
-
return { message: '已生成 B 站视频卡片', images: [base64] };
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ── 查询类公共技能(feedback:结果回喂给 AI 整理回答,不直接发摘要) ──
|
|
360
|
-
|
|
361
|
-
/** MC版本:Minecraft 最新正式版/快照版 */
|
|
362
|
-
private async executeMcVersion(): Promise<string> {
|
|
363
|
-
const version = await MinecraftAPI.getVersion();
|
|
364
|
-
if (!version.success) throw new Error('获取 Minecraft 版本信息失败');
|
|
365
|
-
return `最新正式版:${version.latest.release}\n最新快照版:${version.latest.snapshot}`;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** MC服务器:Java 版服务器状态(地址可带端口,也可分开传) */
|
|
369
|
-
private async executeMcServer({ args }: SkillExecuteContext): Promise<string> {
|
|
370
|
-
const ip = args[0]?.trim();
|
|
371
|
-
if (!ip) throw new Error('缺少服务器地址,格式: MC服务器 <ip> [端口]');
|
|
372
|
-
const parsed = args[1] ? Number.parseInt(args[1], 10) : NaN;
|
|
373
|
-
const status = await MinecraftAPI.getMinecraftServerStatus(ip, Number.isSafeInteger(parsed) ? parsed : undefined);
|
|
374
|
-
if (!status.online) return `服务器 ${ip} 当前离线或无法连接`;
|
|
375
|
-
return [
|
|
376
|
-
`服务器 ${ip} 在线`,
|
|
377
|
-
`MOTD:${status.motd}`,
|
|
378
|
-
`游戏版本:${status.version}`,
|
|
379
|
-
`在线玩家:${status.players.online}/${status.players.max}`,
|
|
380
|
-
status.players.list.length > 0 ? `在线列表:${status.players.list.join('、')}` : ''
|
|
381
|
-
]
|
|
382
|
-
.filter(Boolean)
|
|
383
|
-
.join('\n');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/** MC维基:中文 Minecraft 维基搜索(关键词可含空格) */
|
|
387
|
-
private async executeMcWiki({ args }: SkillExecuteContext): Promise<string> {
|
|
388
|
-
const query = args.join(' ').trim();
|
|
389
|
-
if (!query) throw new Error('缺少关键词,格式: MC维基 <关键词>');
|
|
390
|
-
const wiki = await MinecraftAPI.getMinecraftWiki(query);
|
|
391
|
-
if (!wiki.success) return `未找到「${query}」相关的维基条目`;
|
|
392
|
-
return `条目:${wiki.title}\n摘要:${wiki.desc.trim()}\n链接:${wiki.url}`;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** Modrinth版本:模组最新版本与下载直链 */
|
|
396
|
-
private async executeModrinthVersion({ args }: SkillExecuteContext): Promise<string> {
|
|
397
|
-
const slug = args[0]?.trim();
|
|
398
|
-
if (!slug) throw new Error('缺少模组 slug,格式: Modrinth版本 <slug>');
|
|
399
|
-
const version = await ModrinthAPI.getModVersion(slug);
|
|
400
|
-
if (!version.success) throw new Error(`获取模组 ${slug} 的版本信息失败(slug 是否正确?)`);
|
|
401
|
-
return (
|
|
402
|
-
`模组 ${slug} 最新版本:${version.latest}\n` +
|
|
403
|
-
`下载直链:https://api.modrinth.com/maven/maven/modrinth/${slug}/${version.latest}/${slug}-${version.latest}.jar`
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
GroupAdminApi,
|
|
3
|
+
ProcessedSkillOutput,
|
|
4
|
+
RouteInfo,
|
|
5
|
+
Skill,
|
|
6
|
+
SkillExecuteContext,
|
|
7
|
+
SkillFeedback,
|
|
8
|
+
SkillResult
|
|
9
|
+
} from '@/features/hermes/types';
|
|
10
|
+
import type { QQBot } from '@/index';
|
|
11
|
+
import { Github } from '@/features/github';
|
|
12
|
+
import { Bili } from '@/features/bili';
|
|
13
|
+
import { MinecraftAPI } from '@/features/minecraft';
|
|
14
|
+
import { ModrinthAPI } from '@/features/modrinth';
|
|
15
|
+
import { isGithubEnabledGroup } from '@/config/features';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 技能管理器(移植自 qq-hermes-bridge src/skills.ts,MIT 协议,原作者 Amorter)。
|
|
19
|
+
* 定义 AI 可调用的技能,生成技能提示词,解析并执行 AI 输出中的技能标签。
|
|
20
|
+
* 管理技能仅限有效管理员(hermes admins ∪ management operators)使用;
|
|
21
|
+
* 卡片类公共技能(查Issue / B站视频)所有人可用,产出图片由调用方逐张发送。
|
|
22
|
+
*/
|
|
23
|
+
export class SkillManager {
|
|
24
|
+
/** 技能标签匹配正则:[SKILL:名称 参数...] */
|
|
25
|
+
private static readonly SKILL_TAG_RE = /\[SKILL:([^\]]+)\]/g;
|
|
26
|
+
/** 已注册的技能列表 */
|
|
27
|
+
private readonly skills: Skill[];
|
|
28
|
+
/** 技能名 → 技能定义的快速索引 */
|
|
29
|
+
private readonly skillIndex: Map<string, Skill>;
|
|
30
|
+
|
|
31
|
+
public constructor() {
|
|
32
|
+
this.skills = [
|
|
33
|
+
{
|
|
34
|
+
name: '禁言',
|
|
35
|
+
usage: '禁言 <QQ号> <时长(分钟)>',
|
|
36
|
+
description: '禁言指定群成员,最长 30 天(43200 分钟)',
|
|
37
|
+
adminOnly: true,
|
|
38
|
+
execute: this.executeMute.bind(this)
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: '解除禁言',
|
|
42
|
+
usage: '解除禁言 <QQ号>',
|
|
43
|
+
description: '解除指定成员的禁言',
|
|
44
|
+
adminOnly: true,
|
|
45
|
+
execute: this.executeUnmute.bind(this)
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: '踢出',
|
|
49
|
+
usage: '踢出 <QQ号>',
|
|
50
|
+
description: '将指定成员踢出群聊',
|
|
51
|
+
adminOnly: true,
|
|
52
|
+
execute: this.executeKick.bind(this)
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: '全员禁言',
|
|
56
|
+
usage: '全员禁言 <开/关>',
|
|
57
|
+
description: '开启或关闭全员禁言',
|
|
58
|
+
adminOnly: true,
|
|
59
|
+
execute: this.executeWholeBan.bind(this)
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: '查Issue',
|
|
63
|
+
usage: '查Issue <owner/repo#编号 或 #编号>',
|
|
64
|
+
description: '查询 GitHub Issue/PR 并生成信息卡片(PR 自动识别;省略仓库时默认 Anvil-Dev/AnvilCraft;一条最多 3 个编号)',
|
|
65
|
+
adminOnly: false,
|
|
66
|
+
execute: this.executeIssueCard.bind(this)
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'B站视频',
|
|
70
|
+
usage: 'B站视频 <BV号/av号/b23.tv短链>',
|
|
71
|
+
description: '获取 B 站视频信息并生成卡片(标题/UP主/时长/播放点赞评论数)',
|
|
72
|
+
adminOnly: false,
|
|
73
|
+
execute: this.executeBiliCard.bind(this)
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'MC版本',
|
|
77
|
+
usage: 'MC版本',
|
|
78
|
+
description: '查询 Minecraft 最新正式版与快照版',
|
|
79
|
+
adminOnly: false,
|
|
80
|
+
feedback: true,
|
|
81
|
+
execute: this.executeMcVersion.bind(this)
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'MC服务器',
|
|
85
|
+
usage: 'MC服务器 <ip> [端口]',
|
|
86
|
+
description: '查询 Minecraft Java 服务器状态(MOTD/版本/在线玩家)',
|
|
87
|
+
adminOnly: false,
|
|
88
|
+
feedback: true,
|
|
89
|
+
execute: this.executeMcServer.bind(this)
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'MC维基',
|
|
93
|
+
usage: 'MC维基 <关键词>',
|
|
94
|
+
description: '搜索中文 Minecraft 维基,返回条目摘要与链接',
|
|
95
|
+
adminOnly: false,
|
|
96
|
+
feedback: true,
|
|
97
|
+
execute: this.executeMcWiki.bind(this)
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'Modrinth版本',
|
|
101
|
+
usage: 'Modrinth版本 <slug>',
|
|
102
|
+
description: '查询 Modrinth 模组最新版本与下载直链(slug 如 create-aeronautics)',
|
|
103
|
+
adminOnly: false,
|
|
104
|
+
feedback: true,
|
|
105
|
+
execute: this.executeModrinthVersion.bind(this)
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
this.skillIndex = new Map(this.skills.map(skill => [skill.name, skill]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 构建技能列表提示词,注入到系统提示词中。
|
|
114
|
+
* 按「公共技能」和「管理技能(仅管理员)」分组展示。
|
|
115
|
+
*/
|
|
116
|
+
public buildPrompt(): string {
|
|
117
|
+
if (this.skills.length === 0) return '';
|
|
118
|
+
|
|
119
|
+
const adminSkills = this.skills.filter(skill => skill.adminOnly);
|
|
120
|
+
const publicSkills = this.skills.filter(skill => !skill.adminOnly && !skill.feedback);
|
|
121
|
+
const querySkills = this.skills.filter(skill => !skill.adminOnly && skill.feedback);
|
|
122
|
+
|
|
123
|
+
const lines: string[] = ['## 可用技能'];
|
|
124
|
+
|
|
125
|
+
if (publicSkills.length > 0) {
|
|
126
|
+
lines.push('');
|
|
127
|
+
for (const skill of publicSkills) {
|
|
128
|
+
lines.push(`- \`${skill.usage}\` — ${skill.description}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (querySkills.length > 0) {
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push('### 查询技能(调用后结果会返回给你,请整理成口语化回答后再回复,不要复述原始数据)');
|
|
135
|
+
for (const skill of querySkills) {
|
|
136
|
+
lines.push(`- \`${skill.usage}\` — ${skill.description}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (adminSkills.length > 0) {
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push('### 管理技能(仅管理员可用)');
|
|
143
|
+
for (const skill of adminSkills) {
|
|
144
|
+
lines.push(`- \`${skill.usage}\` — ${skill.description}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('调用格式:在回复中插入 `[SKILL:技能名 参数...]`,标签会在发送前被处理并移除。');
|
|
150
|
+
lines.push('注意:参数中的 QQ 号使用纯数字格式,多个技能可在一段话中同时调用。');
|
|
151
|
+
lines.push('卡片类技能(查Issue / B站视频)会以图片形式随回复一起发出,无需在文本中复述卡片内容。');
|
|
152
|
+
lines.push('查询类技能调用后请等待数据返回,不要假装已经知道结果。');
|
|
153
|
+
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 解析并执行 AI 输出中的 [SKILL:...] 标签。
|
|
159
|
+
* 全部标签执行完毕后从文本中移除,并在末尾附加执行摘要。
|
|
160
|
+
*
|
|
161
|
+
* @param output AI 生成的输出文本
|
|
162
|
+
* @param route 消息来源路由
|
|
163
|
+
* @param deps 依赖项(QQBot、群管理 API 门面和管理员检查函数)
|
|
164
|
+
* @returns 清理后的文本(含执行摘要)与全部技能产出的图片(base64)
|
|
165
|
+
*/
|
|
166
|
+
public async processTags(
|
|
167
|
+
output: string,
|
|
168
|
+
route: RouteInfo,
|
|
169
|
+
deps: { bot: QQBot; api: GroupAdminApi; isAdmin: (userId: string) => boolean }
|
|
170
|
+
): Promise<ProcessedSkillOutput> {
|
|
171
|
+
const { bot, api, isAdmin } = deps;
|
|
172
|
+
|
|
173
|
+
// 收集所有技能标签
|
|
174
|
+
const tags: Array<{ raw: string; content: string }> = [];
|
|
175
|
+
let match: RegExpExecArray | null;
|
|
176
|
+
SkillManager.SKILL_TAG_RE.lastIndex = 0;
|
|
177
|
+
while ((match = SkillManager.SKILL_TAG_RE.exec(output)) !== null) {
|
|
178
|
+
tags.push({ raw: match[0], content: match[1]! });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (tags.length === 0) return { text: output, images: [], feedback: [] };
|
|
182
|
+
|
|
183
|
+
// 逐个解析并执行
|
|
184
|
+
const results: SkillResult[] = [];
|
|
185
|
+
const feedback: SkillFeedback[] = [];
|
|
186
|
+
for (const tag of tags) {
|
|
187
|
+
const parts = tag.content.trim().split(/\s+/);
|
|
188
|
+
const skillName = parts[0]!;
|
|
189
|
+
const args = parts.slice(1);
|
|
190
|
+
|
|
191
|
+
const skill = this.skillIndex.get(skillName);
|
|
192
|
+
if (!skill) {
|
|
193
|
+
results.push({ ok: false, skill: skillName, error: `未知技能: ${skillName}` });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 管理员权限检查
|
|
198
|
+
if (skill.adminOnly && !isAdmin(route.userId)) {
|
|
199
|
+
results.push({ ok: false, skill: skillName, error: '仅管理员可用' });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const execution = await skill.execute({ bot, api, route, args });
|
|
205
|
+
const data = typeof execution === 'string' ? execution : execution.message;
|
|
206
|
+
if (skill.feedback) {
|
|
207
|
+
// 查询类:数据回喂给 AI 整理回答,不附加摘要
|
|
208
|
+
feedback.push({ skill: skillName, data });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (typeof execution === 'string') {
|
|
212
|
+
results.push({ ok: true, skill: skillName, message: execution });
|
|
213
|
+
} else {
|
|
214
|
+
results.push({
|
|
215
|
+
ok: true,
|
|
216
|
+
skill: skillName,
|
|
217
|
+
message: execution.message,
|
|
218
|
+
...(execution.images?.length ? { images: execution.images } : {})
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// 查询类失败同样回喂(让 AI 告知用户没查到),动作类记入错误摘要
|
|
223
|
+
if (skill.feedback) {
|
|
224
|
+
feedback.push({ skill: skillName, data: `执行失败:${(error as Error).message}` });
|
|
225
|
+
} else {
|
|
226
|
+
results.push({ ok: false, skill: skillName, error: (error as Error).message });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 从文本中移除所有技能标签
|
|
232
|
+
let cleaned = output;
|
|
233
|
+
for (const tag of tags) {
|
|
234
|
+
cleaned = cleaned.replace(tag.raw, '');
|
|
235
|
+
}
|
|
236
|
+
cleaned = cleaned.replace(/ {2,}/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
237
|
+
|
|
238
|
+
// 附加执行摘要(整体 trim:整条消息只有技能标签时避免摘要前出现空行——对上游的微调)
|
|
239
|
+
const summaryLines: string[] = [];
|
|
240
|
+
const images: string[] = [];
|
|
241
|
+
for (const result of results) {
|
|
242
|
+
if (result.ok) {
|
|
243
|
+
summaryLines.push(`✅ ${result.message}`);
|
|
244
|
+
if (result.images) images.push(...result.images);
|
|
245
|
+
} else {
|
|
246
|
+
summaryLines.push(`❌ ${result.skill}: ${result.error}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (summaryLines.length > 0) {
|
|
250
|
+
cleaned = cleaned + '\n\n' + summaryLines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { text: cleaned.trim(), images, feedback };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 从参数中提取纯数字 QQ 号 */
|
|
257
|
+
private extractQq(arg: string | undefined): string | null {
|
|
258
|
+
if (!arg) return null;
|
|
259
|
+
const match = String(arg).match(/\((\d+)\)$/);
|
|
260
|
+
return match ? match[1]! : String(arg).replace(/\D/g, '') || null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 解析正整数 */
|
|
264
|
+
private parsePositiveInt(text: string | undefined, max = Infinity): number | null {
|
|
265
|
+
const value = Number.parseInt(text ?? '', 10);
|
|
266
|
+
if (!Number.isFinite(value) || value <= 0) return null;
|
|
267
|
+
return Math.min(value, max);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 执行禁言技能 */
|
|
271
|
+
private async executeMute({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
272
|
+
const qq = this.extractQq(args[0]);
|
|
273
|
+
if (!qq) throw new Error('缺少 QQ 号,格式: 禁言 <QQ号> <时长(分钟)>');
|
|
274
|
+
const minutes = this.parsePositiveInt(args[1], 43200);
|
|
275
|
+
if (!minutes) throw new Error('时长无效,格式: 禁言 <QQ号> <时长(分钟)>');
|
|
276
|
+
await api.setGroupBan(route.groupId!, qq, minutes * 60);
|
|
277
|
+
return `已禁言 ${qq} ${minutes} 分钟`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 执行解除禁言技能 */
|
|
281
|
+
private async executeUnmute({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
282
|
+
const qq = this.extractQq(args[0]);
|
|
283
|
+
if (!qq) throw new Error('缺少 QQ 号,格式: 解除禁言 <QQ号>');
|
|
284
|
+
await api.setGroupBan(route.groupId!, qq, 0);
|
|
285
|
+
return `已解除 ${qq} 的禁言`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** 执行踢出技能 */
|
|
289
|
+
private async executeKick({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
290
|
+
const qq = this.extractQq(args[0]);
|
|
291
|
+
if (!qq) throw new Error('缺少 QQ 号,格式: 踢出 <QQ号>');
|
|
292
|
+
await api.setGroupKick(route.groupId!, qq);
|
|
293
|
+
return `已将 ${qq} 踢出群聊`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 执行全员禁言技能 */
|
|
297
|
+
private async executeWholeBan({ api, route, args }: SkillExecuteContext): Promise<string> {
|
|
298
|
+
const arg = String(args[0] || '').trim();
|
|
299
|
+
if (['开', '开启', '启用', 'on', 'true', '1'].includes(arg)) {
|
|
300
|
+
await api.setGroupWholeBan(route.groupId!, true);
|
|
301
|
+
return '已开启全员禁言';
|
|
302
|
+
}
|
|
303
|
+
if (['关', '关闭', '禁用', 'off', 'false', '0'].includes(arg)) {
|
|
304
|
+
await api.setGroupWholeBan(route.groupId!, false);
|
|
305
|
+
return '已关闭全员禁言';
|
|
306
|
+
}
|
|
307
|
+
throw new Error('参数无效,请使用 开 或 关');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── 卡片类公共技能(复用 github / bili 功能的渲染管线) ──
|
|
311
|
+
|
|
312
|
+
/** 卡片技能一次最多生成的图片数(防刷屏) */
|
|
313
|
+
private static readonly MAX_CARD_IMAGES = 3;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 查Issue:解析编号引用并复用 Github.renderReference 渲染卡片(PR 自动识别)。
|
|
317
|
+
* 群/频道路由要求该群启用 github 功能;仓库允许列表判定在 renderReference 内完成。
|
|
318
|
+
*/
|
|
319
|
+
private async executeIssueCard({ bot, route, args }: SkillExecuteContext): Promise<{ message: string; images?: string[] }> {
|
|
320
|
+
const input = args.join(' ').trim();
|
|
321
|
+
if (!input) throw new Error('缺少编号,格式: 查Issue <owner/repo#编号 或 #编号>');
|
|
322
|
+
if (route.type !== 'user' && route.groupId && !isGithubEnabledGroup(Number(route.groupId))) {
|
|
323
|
+
throw new Error('本群未启用 GitHub 功能');
|
|
324
|
+
}
|
|
325
|
+
const references = Github.parseReferences(input).slice(0, SkillManager.MAX_CARD_IMAGES);
|
|
326
|
+
if (references.length === 0) {
|
|
327
|
+
throw new Error('未识别到 Issue/PR 编号,格式: 查Issue <owner/repo#编号 或 #编号>');
|
|
328
|
+
}
|
|
329
|
+
const images: string[] = [];
|
|
330
|
+
const notes: string[] = [];
|
|
331
|
+
for (const reference of references) {
|
|
332
|
+
const rendered = await Github.renderReference(bot, reference);
|
|
333
|
+
// Message 联合类型中部分成员(如 DiceMessage)无 data 字段,整体断言后取值
|
|
334
|
+
const data = (rendered as { data?: { file?: string; text?: string } }).data ?? {};
|
|
335
|
+
if (rendered.type === 'image') {
|
|
336
|
+
const file = data.file ?? '';
|
|
337
|
+
const comma = file.indexOf(',');
|
|
338
|
+
const base64 = (comma >= 0 ? file.slice(comma + 1) : file).trim();
|
|
339
|
+
images.push(base64);
|
|
340
|
+
notes.push(`${reference.repository}#${reference.number}`);
|
|
341
|
+
} else {
|
|
342
|
+
// 渲染失败/仓库不允许:尚无成功卡片时整体失败;已有成功卡片时并入摘要说明
|
|
343
|
+
const reason = data.text ?? '渲染失败';
|
|
344
|
+
if (images.length === 0) throw new Error(reason);
|
|
345
|
+
notes.push(`${reference.repository}#${reference.number}(${reason})`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { message: `已生成卡片:${notes.join('、')}`, images };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** B站视频:BV号/av号/b23.tv 短链 → Bili.renderVideoCard 渲染信息卡片 */
|
|
352
|
+
private async executeBiliCard({ bot, args }: SkillExecuteContext): Promise<{ message: string; images: string[] }> {
|
|
353
|
+
const input = args.join(' ').trim();
|
|
354
|
+
if (!input) throw new Error('缺少视频号,格式: B站视频 <BV号/av号/b23.tv短链>');
|
|
355
|
+
const base64 = await Bili.renderVideoCard(bot, input);
|
|
356
|
+
return { message: '已生成 B 站视频卡片', images: [base64] };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── 查询类公共技能(feedback:结果回喂给 AI 整理回答,不直接发摘要) ──
|
|
360
|
+
|
|
361
|
+
/** MC版本:Minecraft 最新正式版/快照版 */
|
|
362
|
+
private async executeMcVersion(): Promise<string> {
|
|
363
|
+
const version = await MinecraftAPI.getVersion();
|
|
364
|
+
if (!version.success) throw new Error('获取 Minecraft 版本信息失败');
|
|
365
|
+
return `最新正式版:${version.latest.release}\n最新快照版:${version.latest.snapshot}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** MC服务器:Java 版服务器状态(地址可带端口,也可分开传) */
|
|
369
|
+
private async executeMcServer({ args }: SkillExecuteContext): Promise<string> {
|
|
370
|
+
const ip = args[0]?.trim();
|
|
371
|
+
if (!ip) throw new Error('缺少服务器地址,格式: MC服务器 <ip> [端口]');
|
|
372
|
+
const parsed = args[1] ? Number.parseInt(args[1], 10) : NaN;
|
|
373
|
+
const status = await MinecraftAPI.getMinecraftServerStatus(ip, Number.isSafeInteger(parsed) ? parsed : undefined);
|
|
374
|
+
if (!status.online) return `服务器 ${ip} 当前离线或无法连接`;
|
|
375
|
+
return [
|
|
376
|
+
`服务器 ${ip} 在线`,
|
|
377
|
+
`MOTD:${status.motd}`,
|
|
378
|
+
`游戏版本:${status.version}`,
|
|
379
|
+
`在线玩家:${status.players.online}/${status.players.max}`,
|
|
380
|
+
status.players.list.length > 0 ? `在线列表:${status.players.list.join('、')}` : ''
|
|
381
|
+
]
|
|
382
|
+
.filter(Boolean)
|
|
383
|
+
.join('\n');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** MC维基:中文 Minecraft 维基搜索(关键词可含空格) */
|
|
387
|
+
private async executeMcWiki({ args }: SkillExecuteContext): Promise<string> {
|
|
388
|
+
const query = args.join(' ').trim();
|
|
389
|
+
if (!query) throw new Error('缺少关键词,格式: MC维基 <关键词>');
|
|
390
|
+
const wiki = await MinecraftAPI.getMinecraftWiki(query);
|
|
391
|
+
if (!wiki.success) return `未找到「${query}」相关的维基条目`;
|
|
392
|
+
return `条目:${wiki.title}\n摘要:${wiki.desc.trim()}\n链接:${wiki.url}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Modrinth版本:模组最新版本与下载直链 */
|
|
396
|
+
private async executeModrinthVersion({ args }: SkillExecuteContext): Promise<string> {
|
|
397
|
+
const slug = args[0]?.trim();
|
|
398
|
+
if (!slug) throw new Error('缺少模组 slug,格式: Modrinth版本 <slug>');
|
|
399
|
+
const version = await ModrinthAPI.getModVersion(slug);
|
|
400
|
+
if (!version.success) throw new Error(`获取模组 ${slug} 的版本信息失败(slug 是否正确?)`);
|
|
401
|
+
return (
|
|
402
|
+
`模组 ${slug} 最新版本:${version.latest}\n` +
|
|
403
|
+
`下载直链:https://api.modrinth.com/maven/maven/modrinth/${slug}/${version.latest}/${slug}-${version.latest}.jar`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|