@dsh-plugin/dsh-synapse 0.0.0-stage → 0.1.0-dev.41044818627

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE.md +18 -0
  3. package/README.md +136 -2
  4. package/cordis.patch.yml +3 -0
  5. package/lib/bot.cjs +8314 -0
  6. package/lib/bot.cjs.map +7 -0
  7. package/lib/build.json +1 -0
  8. package/lib/client.js +46 -0
  9. package/lib/host/agent.d.ts +21 -0
  10. package/lib/host/agent.js +151 -0
  11. package/lib/host/agent.js.map +1 -0
  12. package/lib/host/bot.d.ts +17 -0
  13. package/lib/host/bot.js +158 -0
  14. package/lib/host/bot.js.map +1 -0
  15. package/lib/host/client.d.ts +2 -0
  16. package/lib/host/client.js +19 -0
  17. package/lib/host/client.js.map +1 -0
  18. package/lib/host/config.d.ts +24 -0
  19. package/lib/host/config.js +192 -0
  20. package/lib/host/config.js.map +1 -0
  21. package/lib/host/files.d.ts +29 -0
  22. package/lib/host/files.js +188 -0
  23. package/lib/host/files.js.map +1 -0
  24. package/lib/host/gateway.d.ts +33 -0
  25. package/lib/host/gateway.js +237 -0
  26. package/lib/host/gateway.js.map +1 -0
  27. package/lib/host/github.d.ts +47 -0
  28. package/lib/host/github.js +219 -0
  29. package/lib/host/github.js.map +1 -0
  30. package/lib/host/http.d.ts +4 -0
  31. package/lib/host/http.js +41 -0
  32. package/lib/host/http.js.map +1 -0
  33. package/lib/host/index.d.ts +32 -0
  34. package/lib/host/index.js +192 -0
  35. package/lib/host/index.js.map +1 -0
  36. package/lib/host/inference-worker.d.ts +1 -0
  37. package/lib/host/inference-worker.js +72 -0
  38. package/lib/host/inference-worker.js.map +1 -0
  39. package/lib/host/mcp.d.ts +138 -0
  40. package/lib/host/mcp.js +104 -0
  41. package/lib/host/mcp.js.map +1 -0
  42. package/lib/host/memory.d.ts +26 -0
  43. package/lib/host/memory.js +155 -0
  44. package/lib/host/memory.js.map +1 -0
  45. package/lib/host/panel.d.ts +1 -0
  46. package/lib/host/panel.html +174 -0
  47. package/lib/host/panel.js +388 -0
  48. package/lib/host/panel.js.map +1 -0
  49. package/lib/host/runtime.d.ts +21 -0
  50. package/lib/host/runtime.js +145 -0
  51. package/lib/host/runtime.js.map +1 -0
  52. package/lib/host/types.d.ts +112 -0
  53. package/lib/host/types.js +2 -0
  54. package/lib/host/types.js.map +1 -0
  55. package/package.json +94 -4
  56. package/scripts/build.mjs +90 -0
  57. package/scripts/check.mjs +13 -0
  58. package/scripts/dump-npm-latest.mjs +49 -0
  59. package/scripts/model-smoke.mjs +25 -0
  60. package/src/agent.ts +203 -0
  61. package/src/bot.ts +173 -0
  62. package/src/client.ts +20 -0
  63. package/src/config.ts +217 -0
  64. package/src/files.ts +206 -0
  65. package/src/gateway.ts +276 -0
  66. package/src/github.ts +263 -0
  67. package/src/http.ts +41 -0
  68. package/src/index.ts +227 -0
  69. package/src/inference-worker.ts +89 -0
  70. package/src/mcp.ts +112 -0
  71. package/src/memory.ts +178 -0
  72. package/src/panel.html +174 -0
  73. package/src/panel.ts +440 -0
  74. package/src/runtime.ts +164 -0
  75. package/src/types.ts +106 -0
  76. package/test/core.test.mjs +279 -0
  77. package/test/fixtures/mcp-server.mjs +14 -0
  78. package/test/integration.test.mjs +253 -0
  79. package/test/panel.test.mjs +73 -0
  80. package/tsconfig.json +18 -0
  81. package/vendor/gugle-chat-bot/LICENSE +661 -0
  82. package/vendor/gugle-chat-bot/package.json +57 -0
  83. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -0
  84. package/vendor/gugle-chat-bot/src/api.json +4285 -0
  85. package/vendor/gugle-chat-bot/src/command/index.ts +141 -0
  86. package/vendor/gugle-chat-bot/src/config/features.ts +548 -0
  87. package/vendor/gugle-chat-bot/src/config/index.ts +184 -0
  88. package/vendor/gugle-chat-bot/src/config/manager.ts +357 -0
  89. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -0
  90. package/vendor/gugle-chat-bot/src/custom.ts +548 -0
  91. package/vendor/gugle-chat-bot/src/event/index.ts +118 -0
  92. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -0
  93. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -0
  94. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -0
  95. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -0
  96. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -0
  97. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +57 -0
  98. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -0
  99. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -0
  100. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -0
  101. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -0
  102. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -0
  103. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -0
  104. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -0
  105. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -0
  106. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -0
  107. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -0
  108. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -0
  109. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -0
  110. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -0
  111. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -0
  112. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -0
  113. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -0
  114. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -0
  115. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +469 -0
  116. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -0
  117. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -0
  118. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -0
  119. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -0
  120. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -0
  121. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -0
  122. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -0
  123. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -0
  124. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -0
  125. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -0
  126. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -0
  127. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -0
  128. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -0
  129. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -0
  130. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -0
  131. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -0
  132. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -0
  133. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -0
  134. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -0
  135. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -0
  136. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -0
  137. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +452 -0
  138. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -0
  139. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +78 -0
  140. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +58 -0
  141. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -0
  142. package/vendor/gugle-chat-bot/src/image/index.ts +278 -0
  143. package/vendor/gugle-chat-bot/src/index.ts +695 -0
  144. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -0
  145. package/vendor/gugle-chat-bot/src/result.json +359 -0
  146. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -0
  147. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -0
  148. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -0
  149. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -0
  150. package/vendor/gugle-chat-bot/tsconfig.json +46 -0
@@ -0,0 +1,548 @@
1
+ import { bot, QQBot } from '@/index';
2
+ import {
3
+ GroupDecreaseNoticeWSMSG,
4
+ GroupIncreaseNoticeWSMSG,
5
+ GroupMessageWSMSG,
6
+ GroupRequestWSMSG,
7
+ Message,
8
+ NotifyNoticeWSMSG,
9
+ PrivateMessageWSMSG
10
+ } from '@/type';
11
+ import { EventDataManager } from '@/event';
12
+ import { ParenthesesMatching } from '@/features/parentheses';
13
+ import { Github } from '@/features/github';
14
+ import { GitHubBindingManager } from '@/features/github/binding';
15
+ import { Poke } from '@/features/poke';
16
+ import { Arguments, CommandManager, CommandSource } from 'gugle-command';
17
+ import { isBotCommandSource, resolveOperatorId } from '@/command';
18
+ import { MinecraftAPI } from '@/features/minecraft';
19
+ import { ModrinthAPI } from '@/features/modrinth';
20
+ import {
21
+ getFeatureGroups,
22
+ isValidRepositoryName,
23
+ isValidRepositoryOwner,
24
+ subscribeGithubRepository
25
+ } from '@/config/features';
26
+ import { addGithubAllowedRepository, isGithubAllowedPattern } from '@/config';
27
+ import { checkReleasedVersion } from '@/features/version-tracker';
28
+ import { Bili } from '@/features/bili';
29
+ import { Management } from '@/features/management';
30
+ import { Welcome } from '@/features/welcome';
31
+ import { PeakValleyTimer } from '@/features/peak-valley-timer';
32
+ import {
33
+ isRestartedByCommand,
34
+ isRestartInProgress,
35
+ reloadRuntime,
36
+ restartApplication,
37
+ restartStartDelayMs,
38
+ shutdownApplication
39
+ } from '@/features/admin';
40
+ import { DiscordBridge } from '@/features/discord-bridge';
41
+ import { HermesBridge } from '@/features/hermes';
42
+
43
+ class CustomBot {
44
+ private static readonly peakValleyTimer: PeakValleyTimer = PeakValleyTimer.getInstance();
45
+
46
+ /** 重启/关停在发送确认消息后的延迟,确保回执先发出再动进程 */
47
+ private static readonly REBOOT_CONFIRM_DELAY_MS = 500;
48
+
49
+ public static helpCommand(source: CommandSource) {
50
+ source.success(`帮助
51
+ · /mcv:获取 Minecraft 版本信息
52
+ · /server <ip> <port?>:获取 Minecraft 服务器状态
53
+ · /wiki <query>:搜索 Minecraft 维基
54
+ · /github bind <Username>:绑定 GitHub 用户名
55
+ · /github subscribe <owner/repo>:订阅仓库消息推送
56
+ · /github allow <owner|owner/repo>:授权仓库访问(管理员)
57
+ · /pardon <QQ号>:把用户移出黑名单(管理员)
58
+ · /send <msg> [group|channel]:向互通的 Discord 频道/QQ 群发送消息(不填目标时发送到第一个互通条目的对端)
59
+ · /pvtime:查询当前是梁文峰时间还是梁文谷时间
60
+ · /reload:从磁盘重载配置与数据文件(管理员)
61
+ · /reboot:重启应用(管理员)
62
+ · /shutdown:关闭应用(管理员)
63
+ 以上命令均可在互通的 Discord 频道中使用(/ 或 ! 前缀),回复发在 Discord 频道
64
+ 在启用的群/互通频道 @机器人 或命中关键词即可与 AI 对话(停止 / 清除上下文 为控制指令)`);
65
+ }
66
+
67
+ public static peakValleyTimeCommand(source: CommandSource) {
68
+ source.success(CustomBot.peakValleyTimer.getCommandMessage());
69
+ }
70
+
71
+ public static minecraftVersionCommand(source: CommandSource) {
72
+ MinecraftAPI.getVersion().then(version => {
73
+ source.success(
74
+ `Minecraft 版本信息
75
+ · 最新正式版:${version.latest.release}
76
+ · 最新快照版:${version.latest.snapshot}`
77
+ );
78
+ });
79
+ }
80
+
81
+ public static serverCommand(source: CommandSource, ip: string, port: number = 25565) {
82
+ MinecraftAPI.getMinecraftServerStatus(ip, port).then(status => {
83
+ if (!status.online) {
84
+ source.success('服务器已离线');
85
+ return;
86
+ }
87
+ let onlinePlayers = '';
88
+ for (const player of status.players.list) {
89
+ if (onlinePlayers) onlinePlayers += '\n';
90
+ onlinePlayers += ` - ${player}`;
91
+ }
92
+ source.success(
93
+ `${status.motd}
94
+ · 游戏版本:${status.version}
95
+ · 在线玩家:${status.players.online}/${status.players.max}
96
+ · 在线列表:\n` + onlinePlayers
97
+ );
98
+ });
99
+ }
100
+
101
+ public static wikiCommand(source: CommandSource, query: string) {
102
+ MinecraftAPI.getMinecraftWiki(query).then(wiki => {
103
+ if (!wiki.success) {
104
+ source.success('未找到相关结果!');
105
+ return;
106
+ }
107
+ source.success(`${wiki.title}
108
+ ${wiki.desc}
109
+ ${wiki.url}`);
110
+ });
111
+ }
112
+
113
+ public static githubBindCommand(source: CommandSource, username: string) {
114
+ if (!isBotCommandSource(source)) {
115
+ source.fail('GitHub 绑定仅支持在群聊或互通频道中使用');
116
+ return;
117
+ }
118
+
119
+ GitHubBindingManager.bind(source.getUserId(), username)
120
+ .then(result => {
121
+ if (result.state === 'bound') {
122
+ source.success(`GitHub 用户 ${username} 绑定成功`);
123
+ return;
124
+ }
125
+ source.success(`请在 GitHub 用户 ${username} 的 Bio 中添加验证码:${result.code}\n添加后再次执行 /github bind ${username}`);
126
+ })
127
+ .catch(error => {
128
+ source.fail(error instanceof Error ? error.message : String(error));
129
+ });
130
+ }
131
+
132
+ public static githubSubscribeCommand(source: CommandSource, repository: string) {
133
+ if (!isBotCommandSource(source)) {
134
+ source.fail('GitHub 订阅仅支持在群聊或互通频道中使用');
135
+ return;
136
+ }
137
+ if (!isValidRepositoryName(repository)) {
138
+ source.fail(`仓库名格式错误:${repository}\n应为 owner/repo,例如 Anvil-Dev/AnvilCraft`);
139
+ return;
140
+ }
141
+ const groupId = source.getGroupId();
142
+ if (groupId === undefined) {
143
+ source.fail('当前频道未绑定互通 QQ 群,无法订阅');
144
+ return;
145
+ }
146
+ // 与消息查询同一套判定:owner 已绑定 GitHub 账号,或命中允许列表配置
147
+ let allowedText = '';
148
+ if (!Github.isAllowedRepository(repository)) {
149
+ if (!source.isAdmin()) {
150
+ source.fail(
151
+ `仓库 ${repository} 不在允许访问的仓库列表中,无法订阅\n` +
152
+ `如这是你自己的仓库,可先用 /github bind <用户名> 绑定 GitHub 账号,或联系管理员添加`
153
+ );
154
+ return;
155
+ }
156
+ // 管理员可自助开白:把该 owner 加入允许列表后继续订阅
157
+ const owner = repository.split('/')[0]!;
158
+ addGithubAllowedRepository(owner);
159
+ allowedText = `· 已自动将 ${owner} 加入允许访问的仓库列表\n`;
160
+ }
161
+ const subscribers = subscribeGithubRepository(repository, groupId);
162
+ const current = subscribers.includes(groupId) ? `已订阅仓库 ${repository} 的消息推送` : `订阅失败,请重试`;
163
+ source.success(
164
+ `${current}
165
+ · 当前订阅该仓库的群:${subscribers.length > 0 ? subscribers.join('、') : '(无)'}
166
+ ${allowedText}· 若该仓库尚未配置 webhook,请在仓库页面 Settings → Webhooks → Add webhook 添加:
167
+ · Payload URL:https://hook.example.com
168
+ · Content type:application/json
169
+ · Secret:留空
170
+ · Which events would you like to trigger this webhook?:Send me everything.`
171
+ );
172
+ }
173
+
174
+ /**
175
+ * /github allow <owner|owner/repo>:把 GitHub owner 或仓库加入允许访问列表(管理员)。
176
+ * 传入 owner 时授权其下全部仓库,传入 owner/repo 时只授权该仓库。
177
+ */
178
+ /** 管理员运维命令的公共权限校验(QQ 侧 operators / Discord 侧服务器管理员) */
179
+ private static requireAdmin(source: CommandSource): boolean {
180
+ if (!isBotCommandSource(source)) {
181
+ source.fail('该命令仅支持在群聊或互通频道中使用');
182
+ return false;
183
+ }
184
+ if (!source.isAdmin()) {
185
+ source.fail('该命令仅限管理员使用');
186
+ return false;
187
+ }
188
+ return true;
189
+ }
190
+
191
+ /** /reload:从磁盘重载配置与数据文件 */
192
+ public static reloadCommand(source: CommandSource) {
193
+ if (!CustomBot.requireAdmin(source)) return;
194
+ reloadRuntime({
195
+ eventData: () => EventDataManager.reload(),
196
+ githubBindings: () => GitHubBindingManager.reload(),
197
+ extra: [{ name: '峰谷时段计时器', run: () => CustomBot.peakValleyTimer.reloadConfig() }]
198
+ })
199
+ .then(report => {
200
+ const lines = [
201
+ '♻️ 配置与数据已从磁盘重载',
202
+ `· 配置文件:${report.configStores} 个`,
203
+ `· 事件数据(黑名单/版本记录):${report.eventData ? '已重载' : '失败'}`,
204
+ `· GitHub 绑定:${report.githubBindings ? '已重载' : '失败'}`
205
+ ];
206
+ if (report.extras.length > 0) lines.push(`· 其他模块:${report.extras.join('、')}`);
207
+ lines.push('· bot-config.json 中的端口/Token 需重启才生效(/reboot)');
208
+ source.success(lines.join('\n'));
209
+ })
210
+ .catch(error => {
211
+ source.fail(`重载失败:${error instanceof Error ? error.message : String(error)}`);
212
+ });
213
+ }
214
+
215
+ /**
216
+ * /reboot:重启应用(拉起新进程 → 健康检查通过后才退出当前进程)。
217
+ *
218
+ * 关键:健康检查失败时**不退出**当前进程(机器人保持在线),并向触发者回报失败原因。
219
+ * 早期实现 spawn 后无条件 exit,一旦新进程启动失败(如丢失 tsx 加载器导致别名解析失败)
220
+ * 就会让机器人直接离线。
221
+ */
222
+ public static async rebootCommand(source: CommandSource) {
223
+ if (!CustomBot.requireAdmin(source)) return;
224
+ if (isRestartInProgress()) {
225
+ source.fail('已有重启正在进行中,请稍候');
226
+ return;
227
+ }
228
+ source.success('🔄 正在重启应用,请稍候…');
229
+ // 先让确认消息发出去,再执行重启(restartApplication 内部含健康检查与退出缓冲)
230
+ await new Promise(resolve => setTimeout(resolve, CustomBot.REBOOT_CONFIRM_DELAY_MS));
231
+ try {
232
+ const result = await restartApplication();
233
+ if (!result.alive) {
234
+ const detail = result.logTail ? `\n${result.logTail.split('\n').slice(-6).join('\n')}` : '';
235
+ bot.logger?.error(`[Admin] /reboot 失败:新进程 ${result.pid} 已退出,当前进程继续运行${detail}`);
236
+ source.fail(`重启失败(当前进程继续运行):新进程启动后立即退出${detail}`);
237
+ return;
238
+ }
239
+ bot.logger?.info(`[Admin] /reboot 由 ${source.getName()} 触发,新进程 pid=${result.pid},当前进程即将退出`);
240
+ } catch (error) {
241
+ const message = error instanceof Error ? error.message : String(error);
242
+ bot.logger?.error(`[Admin] /reboot 失败: ${message}`);
243
+ source.fail(`重启失败(当前进程继续运行):${message}`);
244
+ }
245
+ }
246
+
247
+ /** /shutdown:关闭应用 */
248
+ public static shutdownCommand(source: CommandSource) {
249
+ if (!CustomBot.requireAdmin(source)) return;
250
+ source.success('🛑 正在关闭应用…');
251
+ bot.logger?.warn(`[Admin] /shutdown 由 ${source.getName()} 触发`);
252
+ setTimeout(() => shutdownApplication(() => bot.stop()), CustomBot.REBOOT_CONFIRM_DELAY_MS);
253
+ }
254
+
255
+ public static githubAllowCommand(source: CommandSource, target: string) {
256
+ if (!isBotCommandSource(source)) {
257
+ source.fail('GitHub allow 仅支持在群聊或互通频道中使用');
258
+ return;
259
+ }
260
+ if (!source.isAdmin()) {
261
+ source.fail('该命令仅限管理员使用');
262
+ return;
263
+ }
264
+ const repositoryTarget = target.includes('/');
265
+ if (repositoryTarget ? !isValidRepositoryName(target) : !isValidRepositoryOwner(target)) {
266
+ source.fail(
267
+ `目标格式错误:${target}\n` +
268
+ `应为 owner(授权其下全部仓库,如 Anvil-Dev)或 owner/repo(仅授权该仓库,如 Anvil-Dev/AnvilCraft)`
269
+ );
270
+ return;
271
+ }
272
+ const existed = isGithubAllowedPattern(target);
273
+ const pattern = addGithubAllowedRepository(target);
274
+ if (existed) {
275
+ source.success(`${pattern} 已在允许访问的仓库列表中`);
276
+ return;
277
+ }
278
+ source.success(
279
+ repositoryTarget
280
+ ? `已将仓库 ${pattern} 加入允许访问的仓库列表`
281
+ : `已将 ${pattern} 加入允许访问的仓库列表,其下全部仓库均可访问`
282
+ );
283
+ }
284
+
285
+ public static pardonCommand(source: CommandSource, userId: string) {
286
+ if (!isBotCommandSource(source)) {
287
+ source.fail('只支持在群聊或互通频道中使用');
288
+ return;
289
+ }
290
+ // 命令层先拦一道:非管理员直接拒绝(QQ = operators 白名单;Discord = 服务器管理员或 dc 白名单)
291
+ if (!source.isAdmin()) {
292
+ source.fail('该命令仅限管理员使用');
293
+ return;
294
+ }
295
+ const groupId = source.getGroupId();
296
+ if (groupId === undefined) {
297
+ source.fail('当前频道未绑定互通 QQ 群,无法赦免');
298
+ return;
299
+ }
300
+ const qq = Number(userId);
301
+ if (!Number.isSafeInteger(qq) || qq <= 0) {
302
+ source.fail(`QQ 号格式错误:${userId}`);
303
+ return;
304
+ }
305
+ // 命令源经 FailOnceCommandSource 代理后不再是 GroupMsgCommandSource 实例,
306
+ // 因此不能用 instanceof 区分平台(否则 QQ 管理员会被当成 Discord 用户判失败)。
307
+ // 改为按 BotCommandSource 的 id 约定推导(纯数字=QQ 号,dc: 前缀=Discord 用户)。
308
+ const operatorId = resolveOperatorId(source);
309
+ Management.pardon(groupId, operatorId, qq)
310
+ .then(removed => {
311
+ if (removed) {
312
+ source.success(`已从黑名单中移除用户 ${qq}`);
313
+ } else {
314
+ source.fail('用户不在黑名单中,或你没有赦免权限');
315
+ }
316
+ })
317
+ .catch(error => {
318
+ source.fail(error instanceof Error ? error.message : String(error));
319
+ });
320
+ }
321
+
322
+ @bot.subscribe('notice-event-notify', false)
323
+ public listenPokeMsg(bot: QQBot, msg: NotifyNoticeWSMSG): void {
324
+ if (!msg.sub_type || msg.sub_type != 'poke') return;
325
+ const sentMessage: Message[] = [
326
+ {
327
+ type: 'at',
328
+ data: {
329
+ qq: msg.user_id
330
+ }
331
+ },
332
+ {
333
+ type: 'text',
334
+ data: {
335
+ text: ' '
336
+ }
337
+ }
338
+ ];
339
+ Poke.processPokeMsg(bot, msg, sentMessage);
340
+ if (sentMessage.length > 2) {
341
+ if (msg.group_id) {
342
+ bot.sendGroupMsg(msg.group_id, sentMessage);
343
+ } else {
344
+ bot.sendPrivateMsg(msg.user_id, sentMessage);
345
+ }
346
+ }
347
+ }
348
+
349
+ @bot.subscribe('message-event-group', false)
350
+ public listenGroupMsg(bot: QQBot, msg: GroupMessageWSMSG): void {
351
+ const sentMessage: Message[] = [
352
+ {
353
+ type: 'reply',
354
+ data: {
355
+ id: msg.message_id
356
+ }
357
+ }
358
+ ];
359
+ ParenthesesMatching.parenthesesMatching(msg, sentMessage);
360
+ // 各处理器失败只记录日志:既不能产生未处理的 rejection(会终止进程),
361
+ // 也不能中断后续处理器与最终的消息发送
362
+ Github.processMessage(bot, msg, sentMessage)
363
+ .catch(e => {
364
+ bot.logger?.error(`Github message process failed: ${e?.message ?? e}`);
365
+ })
366
+ .then(() => Bili.processMessage(bot, msg, sentMessage))
367
+ .catch(e => {
368
+ bot.logger?.error(`Bili message process failed: ${e?.message ?? e}`);
369
+ })
370
+ .then(() => {
371
+ if (sentMessage.length > 1) {
372
+ bot.sendGroupMsg(msg.group_id, sentMessage);
373
+ }
374
+ });
375
+ }
376
+
377
+ @bot.subscribe('message-event-group', false)
378
+ public listenDiscordBridgeMsg(bot: QQBot, msg: GroupMessageWSMSG): void {
379
+ // QQ ⇄ Discord 互通:转发与 /send 分发(不依赖 command 白名单,内部自行门控)
380
+ DiscordBridge.getInstance().handleQQMessage(bot, msg);
381
+ }
382
+
383
+ @bot.subscribe('message-event-group', false)
384
+ public listenHermesGroupMsg(bot: QQBot, msg: GroupMessageWSMSG): void {
385
+ // QQ ⇄ Hermes Agent 桥接:@提及/关键词触发 AI 对话(内部自行门控:hermes.json groups)
386
+ HermesBridge.getInstance().handleGroupMessage(bot, msg);
387
+ }
388
+
389
+ @bot.subscribe('message-event-private', false)
390
+ public listenHermesPrivateMsg(bot: QQBot, msg: PrivateMessageWSMSG): void {
391
+ // Hermes 私聊始终可触发(受 hermes.json 用户黑白名单约束)
392
+ HermesBridge.getInstance().handlePrivateMessage(bot, msg);
393
+ }
394
+
395
+ @bot.subscribe('notice-event-group-decrease', false)
396
+ public listenGroupDecreaseNotice(bot: QQBot, msg: GroupDecreaseNoticeWSMSG): void {
397
+ Management.handleGroupDecreaseNotice(bot, msg);
398
+ }
399
+
400
+ @bot.subscribe('notice-event-group-increase', false)
401
+ public listenGroupIncreaseNotice(bot: QQBot, msg: GroupIncreaseNoticeWSMSG): void {
402
+ Management.handleGroupIncreaseNotice(bot, msg);
403
+ Welcome.handleGroupIncreaseNotice(bot, msg);
404
+ }
405
+
406
+ @bot.subscribe('request-event-group', false)
407
+ public listenGroupRequest(bot: QQBot, msg: GroupRequestWSMSG): void {
408
+ Management.handleGroupRequest(bot, msg);
409
+ }
410
+
411
+ @bot.subscribe('command-register', false)
412
+ public cmd(bot: QQBot, command: CommandManager) {
413
+ command.register('gugle-command', CommandManager.literal('help').execute(CustomBot.helpCommand));
414
+ command.register('gugle-command', CommandManager.literal('mcv').execute(CustomBot.minecraftVersionCommand));
415
+ command.register(
416
+ 'gugle-command',
417
+ CommandManager.literal('server').then(
418
+ CommandManager.argument('ip', Arguments.STRING)
419
+ .execute(CustomBot.serverCommand)
420
+ .then(CommandManager.argument('port', Arguments.NUMBER).execute(CustomBot.serverCommand))
421
+ )
422
+ );
423
+ command.register(
424
+ 'gugle-command',
425
+ CommandManager.literal('wiki').then(
426
+ CommandManager.argument('query', Arguments.STRING).execute(CustomBot.wikiCommand)
427
+ )
428
+ );
429
+ command.register(
430
+ 'gugle-command',
431
+ CommandManager.literal('github')
432
+ .then(
433
+ CommandManager.literal('bind').then(
434
+ CommandManager.argument('username', Arguments.STRING).execute(CustomBot.githubBindCommand)
435
+ )
436
+ )
437
+ .then(
438
+ CommandManager.literal('subscribe').then(
439
+ CommandManager.argument('repository', Arguments.STRING).execute(CustomBot.githubSubscribeCommand)
440
+ )
441
+ )
442
+ .then(
443
+ CommandManager.literal('allow').then(
444
+ CommandManager.argument('target', Arguments.STRING).execute(CustomBot.githubAllowCommand)
445
+ )
446
+ )
447
+ );
448
+ command.register(
449
+ 'gugle-command',
450
+ CommandManager.literal('pardon').then(
451
+ CommandManager.argument('userId', Arguments.STRING).execute(CustomBot.pardonCommand)
452
+ )
453
+ );
454
+ command.register('gugle-command', CommandManager.literal('pvtime').execute(CustomBot.peakValleyTimeCommand));
455
+ // 管理员运维命令:权限在命令实现内校验(QQ operators / Discord 服务器管理员)
456
+ command.register('gugle-command', CommandManager.literal('reboot').execute(CustomBot.rebootCommand));
457
+ command.register('gugle-command', CommandManager.literal('reload').execute(CustomBot.reloadCommand));
458
+ command.register('gugle-command', CommandManager.literal('shutdown').execute(CustomBot.shutdownCommand));
459
+ }
460
+
461
+ @bot.subscribe('after-start', false)
462
+ public startPeakValleyTimer(bot: QQBot): void {
463
+ if (isRestartedByCommand()) {
464
+ bot.logger?.info('[Admin] 本进程由 /reboot 拉起(上一进程已退出)');
465
+ }
466
+ CustomBot.peakValleyTimer.start(bot);
467
+ DiscordBridge.getInstance().start(bot);
468
+ // 清理上次进程在 AI 处理中退出时残留的 QQ 表情回应(Discord 残留在 clientReady 后清理)
469
+ HermesBridge.getInstance().cleanupStaleQQReactions(bot).catch(error => {
470
+ bot.logger?.error(`[Hermes] 残留表情回应清理失败: ${error instanceof Error ? error.message : String(error)}`);
471
+ });
472
+ }
473
+
474
+ @bot.cron('*/30 * * * * *')
475
+ public cronCheckMinecraftVersion() {
476
+ MinecraftAPI.getVersion().then(version => {
477
+ if (!version.success) return;
478
+ // 从 manifest 中按 id 查找 release / snapshot 的权威 releaseTime,
479
+ // 用时间戳单调性判断新发布,避免 CDN 缓存抖动重复发送
480
+ const releaseId = version.latest.release;
481
+ const snapshotId = version.latest.snapshot;
482
+ const releaseTime = version.versions.find(v => v.id === releaseId)?.releaseTime || '';
483
+ const snapshotTime = version.versions.find(v => v.id === snapshotId)?.releaseTime || '';
484
+ Promise.all([
485
+ checkReleasedVersion('mcupdate:release', releaseId, Date.parse(releaseTime) || 0),
486
+ checkReleasedVersion('mcupdate:snapshot', snapshotId, Date.parse(snapshotTime) || 0)
487
+ ]).then(([releaseNew, snapshotNew]) => {
488
+ const msg: Message[] = [
489
+ {
490
+ type: 'text',
491
+ data: {
492
+ text: '发现新版本!\n'
493
+ }
494
+ }
495
+ ];
496
+ if (releaseNew) {
497
+ msg.push({
498
+ type: 'text',
499
+ data: {
500
+ text: `· 最新正式版:${version.latest.release}\n`
501
+ }
502
+ });
503
+ msg.push({
504
+ type: 'text',
505
+ data: {
506
+ text: `https://www.minecraft.net/en-us/article/minecraft-java-edition-${version.latest.release.replace('.', '-')}`
507
+ }
508
+ });
509
+ }
510
+ if (snapshotNew) {
511
+ msg.push({
512
+ type: 'text',
513
+ data: {
514
+ text: `· 最新快照版:${version.latest.snapshot}\n`
515
+ }
516
+ });
517
+ let snapshot = version.latest.snapshot;
518
+ if (snapshot.includes('pre')) {
519
+ snapshot = snapshot.replace('pre', 'pre-release');
520
+ } else if (snapshot.includes('rc')) {
521
+ snapshot = snapshot.replace('rc', 'release-candidate');
522
+ }
523
+ snapshot = snapshot.replace('.', '-');
524
+ msg.push({
525
+ type: 'text',
526
+ data: {
527
+ text: `https://www.minecraft.net/en-us/article/minecraft-${snapshot}`
528
+ }
529
+ });
530
+ }
531
+ // 只有 release 或 snapshot 任一被确认为新发布才发送
532
+ if (!releaseNew && !snapshotNew) return;
533
+ for (const listener of getFeatureGroups('minecraft')) {
534
+ bot.sendGroupMsg(listener, msg);
535
+ }
536
+ });
537
+ });
538
+ }
539
+
540
+ // @bot.cron('0/30 * * * * *')
541
+ // public cronCheckAeronauticsVersion() {
542
+ // ModrinthAPI.checkVersion(bot, 'create-aeronautics', '航空学');
543
+ // }
544
+ }
545
+
546
+ export default function run() {
547
+ return new CustomBot();
548
+ }
@@ -0,0 +1,118 @@
1
+ import * as fs from 'node:fs';
2
+ import { readOrCreate } from '@/config/manager';
3
+
4
+ export class EventDataManager {
5
+ private static readonly FILE: string = './configs/event-data.json';
6
+ private static readonly LEGACY_FILE: string = './config/event-data.json';
7
+ private static readonly LEGACY_DIRECTORY: string = './config';
8
+ private static listeners: {
9
+ [key: string]: { roomId: string; channelId: string }[];
10
+ } = {};
11
+
12
+ private static storage: {
13
+ [key: string]: {
14
+ [key: string]: any;
15
+ };
16
+ } = {};
17
+
18
+ static {
19
+ EventDataManager.load().then();
20
+ }
21
+
22
+ public static async addListener(event: string, roomId: string, channelId: string): Promise<void> {
23
+ if (!EventDataManager.listeners[event]) EventDataManager.listeners[event] = [];
24
+ EventDataManager.listeners[event].push({ roomId, channelId });
25
+ await EventDataManager.save();
26
+ }
27
+
28
+ public static async containListener(event: string, roomId: string, channelId: string): Promise<boolean> {
29
+ if (!EventDataManager.listeners[event]) return false;
30
+ return EventDataManager.listeners[event].some(
31
+ listener => listener.roomId === roomId && listener.channelId === channelId
32
+ );
33
+ }
34
+
35
+ public static async removeListener(event: string, roomId: string, channelId: string): Promise<void> {
36
+ if (!EventDataManager.listeners[event]) return;
37
+ EventDataManager.listeners[event] = EventDataManager.listeners[event].filter(
38
+ listener => listener.roomId !== roomId && listener.channelId !== channelId
39
+ );
40
+ await EventDataManager.save();
41
+ }
42
+
43
+ public static async getListeners(event: string): Promise<{ roomId: string; channelId: string }[]> {
44
+ return EventDataManager.listeners[event] || [];
45
+ }
46
+
47
+ public static async setStorage(event: string, key: string, value: any): Promise<void> {
48
+ if (!EventDataManager.storage[event]) EventDataManager.storage[event] = {};
49
+ EventDataManager.storage[event][key] = value;
50
+ await EventDataManager.save();
51
+ }
52
+
53
+ public static async getStorage(event: string, key: string): Promise<any> {
54
+ if (!EventDataManager.storage[event]) return undefined;
55
+ return EventDataManager.storage[event][key];
56
+ }
57
+
58
+ public static async clearStorage(event: string): Promise<void> {
59
+ delete EventDataManager.storage[event];
60
+ await EventDataManager.save();
61
+ }
62
+
63
+ /**
64
+ * 从磁盘重新加载事件数据(/reload 用)。
65
+ * 现有 storage 会被磁盘内容覆盖,因此同时清空已删除的键。
66
+ */
67
+ public static async reload(): Promise<void> {
68
+ EventDataManager.listeners = {};
69
+ EventDataManager.storage = {};
70
+ await EventDataManager.load();
71
+ }
72
+
73
+ private static migrateLegacyFile(): void {
74
+ if (!fs.existsSync(EventDataManager.LEGACY_DIRECTORY)) return;
75
+
76
+ fs.mkdirSync('./configs', { recursive: true });
77
+ if (fs.existsSync(EventDataManager.LEGACY_FILE) && !fs.existsSync(EventDataManager.FILE)) {
78
+ fs.renameSync(EventDataManager.LEGACY_FILE, EventDataManager.FILE);
79
+ }
80
+ fs.rmSync(EventDataManager.LEGACY_DIRECTORY, { recursive: true, force: true });
81
+ }
82
+
83
+ public static async checkAndCreateFile(): Promise<void> {
84
+ EventDataManager.migrateLegacyFile();
85
+ readOrCreate<{ listeners: Record<string, never>; storage: Record<string, never> }>({
86
+ path: EventDataManager.FILE,
87
+ factory: () => ({ listeners: {}, storage: {} })
88
+ });
89
+ }
90
+
91
+ public static async load(): Promise<void> {
92
+ await EventDataManager.checkAndCreateFile();
93
+ const data = readOrCreate<{
94
+ listeners?: Record<string, { roomId: string; channelId: string }[]>;
95
+ storage?: Record<string, Record<string, unknown>>;
96
+ }>({
97
+ path: EventDataManager.FILE,
98
+ factory: () => ({ listeners: {}, storage: {} })
99
+ }).data;
100
+ EventDataManager.listeners = {
101
+ ...EventDataManager.listeners,
102
+ ...(data.listeners || {})
103
+ };
104
+ EventDataManager.storage = {
105
+ ...EventDataManager.storage,
106
+ ...(data.storage || {})
107
+ };
108
+ }
109
+
110
+ public static async save(): Promise<void> {
111
+ await EventDataManager.checkAndCreateFile();
112
+ const { writeConfigFile } = await import('@/config/manager');
113
+ writeConfigFile(EventDataManager.FILE, {
114
+ listeners: EventDataManager.listeners,
115
+ storage: EventDataManager.storage
116
+ });
117
+ }
118
+ }