@sfmc-bds/qq-bridge 0.1.1-beta.4 → 0.2.0-beta.4

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 (62) hide show
  1. package/README.md +14 -3
  2. package/dist/commands/db-api.js +89 -0
  3. package/dist/commands/db-api.js.map +7 -0
  4. package/dist/commands/handlers.js +660 -0
  5. package/dist/commands/handlers.js.map +7 -0
  6. package/dist/commands/index.js +55 -0
  7. package/dist/commands/index.js.map +7 -0
  8. package/dist/commands/interaction-router.js +149 -0
  9. package/dist/commands/interaction-router.js.map +7 -0
  10. package/dist/commands/join-settings-ui.js +108 -0
  11. package/dist/commands/join-settings-ui.js.map +7 -0
  12. package/dist/commands/join-settings-ui.test.js +43 -0
  13. package/dist/commands/join-settings-ui.test.js.map +7 -0
  14. package/dist/commands/menu-format.js +45 -0
  15. package/dist/commands/menu-format.js.map +7 -0
  16. package/dist/commands/menu-panel-sync.js +130 -0
  17. package/dist/commands/menu-panel-sync.js.map +7 -0
  18. package/dist/commands/menu-panel-sync.test.js +27 -0
  19. package/dist/commands/menu-panel-sync.test.js.map +7 -0
  20. package/dist/commands/pending.js +33 -0
  21. package/dist/commands/pending.js.map +7 -0
  22. package/dist/commands/registry.js +40 -0
  23. package/dist/commands/registry.js.map +7 -0
  24. package/dist/commands/render.js +46 -0
  25. package/dist/commands/render.js.map +7 -0
  26. package/dist/commands/render.test.js +44 -0
  27. package/dist/commands/render.test.js.map +7 -0
  28. package/dist/commands/reply-llbot.js +63 -0
  29. package/dist/commands/reply-llbot.js.map +7 -0
  30. package/dist/commands/reply-official.js +66 -0
  31. package/dist/commands/reply-official.js.map +7 -0
  32. package/dist/commands/router.js +53 -0
  33. package/dist/commands/router.js.map +7 -0
  34. package/dist/commands/router.test.js +92 -0
  35. package/dist/commands/router.test.js.map +7 -0
  36. package/dist/commands/types.js +1 -0
  37. package/dist/commands/types.js.map +7 -0
  38. package/dist/config.js +12 -2
  39. package/dist/config.js.map +2 -2
  40. package/dist/console.js +40 -20
  41. package/dist/console.js.map +2 -2
  42. package/dist/index.js +117 -20
  43. package/dist/index.js.map +2 -2
  44. package/dist/llbot-ws-api.js +90 -0
  45. package/dist/llbot-ws-api.js.map +7 -0
  46. package/dist/llbot-ws-api.test.js +32 -0
  47. package/dist/llbot-ws-api.test.js.map +7 -0
  48. package/dist/official/events.js +136 -0
  49. package/dist/official/events.js.map +7 -0
  50. package/dist/official/events.test.js +123 -0
  51. package/dist/official/events.test.js.map +7 -0
  52. package/dist/official/gateway.js +226 -0
  53. package/dist/official/gateway.js.map +7 -0
  54. package/dist/onebot.js +23 -5
  55. package/dist/onebot.js.map +2 -2
  56. package/dist/project-root.js +2 -2
  57. package/dist/project-root.js.map +2 -2
  58. package/dist/runtime-status.js +45 -0
  59. package/dist/runtime-status.js.map +7 -0
  60. package/dist/ws-server.js +4 -0
  61. package/dist/ws-server.js.map +2 -2
  62. package/package.json +6 -5
@@ -0,0 +1,660 @@
1
+ import {
2
+ getGroupBotState,
3
+ getGroupInfo,
4
+ sendGroupMessage
5
+ } from "@sfmc-bds/sdk/node/qq-official";
6
+ import {
7
+ fetchBindMe,
8
+ fetchJoinPending,
9
+ fetchJoinSettings,
10
+ fetchSfmcStatus,
11
+ postAdminKick,
12
+ postBindRequest,
13
+ postBindUnbind,
14
+ postJoinDecide,
15
+ postJoinRequest,
16
+ postJoinSettings
17
+ } from "./db-api.js";
18
+ import { buildJoinSettingsPanel, settingsFromResponse } from "./join-settings-ui.js";
19
+ import { formatCommandMenu } from "./menu-format.js";
20
+ function formatUptime(startedAt) {
21
+ const ms = Math.max(0, Date.now() - startedAt);
22
+ const s = Math.floor(ms / 1e3);
23
+ const m = Math.floor(s / 60);
24
+ const h = Math.floor(m / 60);
25
+ if (h > 0) return `${h}h ${m % 60}m`;
26
+ if (m > 0) return `${m}m ${s % 60}s`;
27
+ return `${s}s`;
28
+ }
29
+ function dbEp(ctx) {
30
+ const host = ctx.runtimeInfo.dbHost;
31
+ const port = ctx.runtimeInfo.dbPort;
32
+ if (!host || !port) return null;
33
+ return { host, port };
34
+ }
35
+ function isAdmin(ctx) {
36
+ const admins = ctx.runtimeInfo.adminOpenids ?? [];
37
+ if (admins.length > 0 && admins.includes(ctx.inbound.userId)) return true;
38
+ return ctx.inbound.isGroupAdmin === true;
39
+ }
40
+ function asGroupAdminField(ctx) {
41
+ return ctx.inbound.isGroupAdmin === true;
42
+ }
43
+ async function fetchQqGroupLines(ctx) {
44
+ const creds = ctx.runtimeInfo.officialCreds;
45
+ const gid = ctx.runtimeInfo.groupOpenid || ctx.inbound.groupId;
46
+ if (!creds || !gid || ctx.inbound.backend !== "official") return [];
47
+ const lines = [];
48
+ try {
49
+ const [infoRes, stateRes] = await Promise.all([
50
+ getGroupInfo(creds, gid),
51
+ getGroupBotState(creds, gid)
52
+ ]);
53
+ if (infoRes.ok && infoRes.json && typeof infoRes.json === "object") {
54
+ const j = infoRes.json;
55
+ lines.push(`QQ\u7FA4\uFF1A${j.group_name ?? "\u2014"} \xB7 \u6210\u5458 ${j.group_member_num ?? "\u2014"}`);
56
+ } else if (!infoRes.ok) {
57
+ const hint = /11253/.test(infoRes.error || infoRes.body || "") ? "\u7FA4\u4FE1\u606F\u63A5\u53E3\u672A\u5F00\u901A\u767D\u540D\u5355(11253)" : `\u7FA4\u4FE1\u606F\u5931\u8D25\uFF1A${(infoRes.error || "").slice(0, 60)}`;
58
+ lines.push(hint);
59
+ }
60
+ if (stateRes.ok && stateRes.json && typeof stateRes.json === "object") {
61
+ const j = stateRes.json;
62
+ lines.push(
63
+ `\u673A\u5668\u4EBA\uFF1A\u89D2\u8272=${j.member_role ?? "\u2014"} \xB7 \u4E3B\u52A8\u63A8\u9001=${j.allow_proactive_msg === true ? "\u5F00" : "\u5173"} \xB7 \u6536\u6D88\u606F=${j.recv_msg_setting ?? "\u2014"}`
64
+ );
65
+ }
66
+ } catch (e) {
67
+ lines.push(`\u7FA4\u4FE1\u606F\u5F02\u5E38\uFF1A${e.message}`);
68
+ }
69
+ return lines;
70
+ }
71
+ function createMenuHandler(registry) {
72
+ return () => {
73
+ const cmds = registry.userMenu().filter((c) => c.name !== "menu");
74
+ return formatCommandMenu({
75
+ title: "SFMC \u6307\u4EE4",
76
+ subtitle: "\u70B9\u6309\u94AE\u6216\u53D1\u540D\u79F0/\u7F16\u53F7\uFF1B\u7BA1\u7406\u5458\u53D1\u300C\u7BA1\u7406\u300D\u6253\u5F00\u7BA1\u7406\u83DC\u5355\u3002",
77
+ cmds,
78
+ idPrefix: "cmd",
79
+ footerMd: "_\u5B98\u65B9\u4EC5 @\u673A\u5668\u4EBA \u89E6\u53D1\uFF1B\u6E38\u620F\u804A\u5929\u9700\u914D\u7F6E `bridge_channel_id`\u3002_",
80
+ footerText: "\u5B98\u65B9\u4EC5 @\u673A\u5668\u4EBA \u89E6\u53D1\uFF1B\u6E38\u620F\u804A\u5929\u9700\u914D\u7F6E bridge_channel_id\u3002"
81
+ });
82
+ };
83
+ }
84
+ function createAdminMenuHandler(registry) {
85
+ return (ctx) => {
86
+ if (!isAdmin(ctx)) {
87
+ return { text: "\u4EC5\u7BA1\u7406\u5458\u53EF\u6253\u5F00\u7BA1\u7406\u83DC\u5355\uFF08\u9700\u5728 qq_admin_openids\uFF0C\u6216\u5F00\u542F\u7FA4\u7BA1\u89C6\u4F5C\u7BA1\u7406\u5458\uFF09" };
88
+ }
89
+ const cmds = registry.adminMenu();
90
+ if (cmds.length === 0) {
91
+ return { text: "\u6682\u65E0\u7BA1\u7406\u6307\u4EE4" };
92
+ }
93
+ return formatCommandMenu({
94
+ title: "SFMC \u7BA1\u7406",
95
+ subtitle: "\u4EE5\u4E0B\u4E3A\u7BA1\u7406\u6307\u4EE4\uFF1B\u4E5F\u53EF\u76F4\u63A5\u53D1\u300C\u8E22\u4EBA\u300D\u300C\u5F85\u5BA1\u300D\u7B49\u89E6\u53D1\u8BCD\u3002",
96
+ cmds,
97
+ idPrefix: "adm",
98
+ footerMd: "_\u654F\u611F\u64CD\u4F5C\u8BF7\u786E\u8BA4\u5BF9\u8C61\u6B63\u786E\u3002_",
99
+ footerText: "\u654F\u611F\u64CD\u4F5C\u8BF7\u786E\u8BA4\u5BF9\u8C61\u6B63\u786E\u3002"
100
+ });
101
+ };
102
+ }
103
+ const pingHandler = (ctx) => {
104
+ const { runtimeInfo, startedAt, inbound } = ctx;
105
+ const parts = ["pong", `backend=${inbound.backend}`, `uptime=${formatUptime(startedAt)}`];
106
+ if (inbound.backend === "official") {
107
+ parts.push(`sandbox=${runtimeInfo.sandbox ? "true" : "false"}`);
108
+ if (runtimeInfo.appIdHint) parts.push(`app=${runtimeInfo.appIdHint}`);
109
+ }
110
+ return { text: parts.join(" \xB7 ") };
111
+ };
112
+ const whoamiHandler = async (ctx) => {
113
+ const { inbound } = ctx;
114
+ const lines = [`\u4F60\u662F ${inbound.userName}`, `id=${inbound.userId}`, `group=${inbound.groupId}`];
115
+ const ep = dbEp(ctx);
116
+ if (ep) {
117
+ try {
118
+ const me = await fetchBindMe(ep, inbound.userId);
119
+ if (me.bound && me.binding) {
120
+ lines.push(`\u5DF2\u7ED1\u5B9A MC\uFF1A${me.binding.player_name || "?"} (${me.binding.player_xuid || "?"})`);
121
+ } else {
122
+ lines.push("\u672A\u7ED1\u5B9A MC\uFF08\u53D1\u9001\u300C\u7ED1\u5B9A\u300D\u83B7\u53D6\u9A8C\u8BC1\u7801\uFF09");
123
+ }
124
+ } catch {
125
+ lines.push("\u7ED1\u5B9A\u67E5\u8BE2\u5931\u8D25\uFF08db-server \u4E0D\u53EF\u8FBE\uFF09");
126
+ }
127
+ }
128
+ return { text: lines.join("\n") };
129
+ };
130
+ const statusHandler = async (ctx) => {
131
+ const ep = dbEp(ctx);
132
+ if (!ep) return { text: "status\uFF1A\u672A\u914D\u7F6E db_host/db_port" };
133
+ try {
134
+ const st = await fetchSfmcStatus(ep);
135
+ const n = Array.isArray(st.online) ? st.online.length : 0;
136
+ const day = st.world?.day != null ? String(st.world.day) : "\u2014";
137
+ const diff = st.world?.difficulty ? String(st.world.difficulty) : "\u2014";
138
+ const age = typeof st.updatedAt === "number" ? `${Math.max(0, Math.floor((Date.now() - st.updatedAt) / 1e3))}s\u524D` : "\u2014";
139
+ const host = st.host;
140
+ const bds = st.processes?.bds;
141
+ const db = st.processes?.db;
142
+ const hostUp = host?.uptimeText || "\u2014";
143
+ const bdsLine = bds?.state === "running" ? `\u8FD0\u884C\u4E2D \xB7 ${bds.uptimeText || "\u2014"}${bds.pid ? ` (PID ${bds.pid})` : ""}` : "\u672A\u8FD0\u884C";
144
+ const mem = host?.memory != null ? `${host.memory.usedMb ?? "\u2014"}/${host.memory.totalMb ?? "\u2014"} MB (${host.memory.usedPercent ?? "\u2014"}%)` : "\u2014";
145
+ const cpu = host?.cpu != null ? `${host.cpu.cores ?? "\u2014"} \u6838 \xB7 ${host.cpu.model || "\u2014"}` : "\u2014";
146
+ const machine = host ? `${host.hostname || "\u2014"} \xB7 ${host.platform || "\u2014"}/${host.arch || "\u2014"} ${host.release || ""}`.trim() : "\u2014";
147
+ const note = st.note ? `
148
+ ${st.note}` : n === 0 ? "\n\u6682\u65E0\u5728\u7EBF\u73A9\u5BB6\u6570\u636E" : "";
149
+ const qqLines = await fetchQqGroupLines(ctx);
150
+ const text = [
151
+ "\u670D\u52A1\u5668\u72B6\u6001",
152
+ `\u5728\u7EBF\uFF1A${n} \u4EBA`,
153
+ `\u4E16\u754C\u65E5\uFF1A${day}`,
154
+ `\u96BE\u5EA6\uFF1A${diff}`,
155
+ `\u4E3B\u673A\uFF1A${machine}`,
156
+ `\u4E3B\u673A\u8FD0\u884C\uFF1A${hostUp}`,
157
+ `BDS\uFF1A${bdsLine}`,
158
+ `\u5185\u5B58\uFF1A${mem}`,
159
+ `CPU\uFF1A${cpu}`,
160
+ `db-server\uFF1A${db?.uptimeText || "\u2014"}${db?.pid ? ` (PID ${db.pid})` : ""}`,
161
+ ...qqLines,
162
+ `\u6570\u636E\uFF1A${age}${note}`
163
+ ].join("\n");
164
+ const markdown = [
165
+ "## \u670D\u52A1\u5668\u72B6\u6001",
166
+ "",
167
+ `- **\u5728\u7EBF**\uFF1A${n} \u4EBA`,
168
+ `- **\u4E16\u754C\u65E5**\uFF1A${day}`,
169
+ `- **\u96BE\u5EA6**\uFF1A${diff}`,
170
+ `- **\u4E3B\u673A**\uFF1A${machine}`,
171
+ `- **\u4E3B\u673A\u8FD0\u884C**\uFF1A${hostUp}`,
172
+ `- **BDS**\uFF1A${bdsLine}`,
173
+ `- **\u5185\u5B58**\uFF1A${mem}`,
174
+ `- **CPU**\uFF1A${cpu}`,
175
+ `- **db-server**\uFF1A${db?.uptimeText || "\u2014"}${db?.pid ? ` (PID ${db.pid})` : ""}`,
176
+ ...qqLines.map((l) => `- ${l}`),
177
+ `- **\u6570\u636E**\uFF1A${age}`,
178
+ note ? `
179
+ _${(st.note || "\u6682\u65E0\u5728\u7EBF\u73A9\u5BB6\u6570\u636E").trim()}_` : ""
180
+ ].filter((line) => line !== "").join("\n");
181
+ return { text, markdown };
182
+ } catch (e) {
183
+ return { text: `status \u5931\u8D25\uFF1A${e.message}` };
184
+ }
185
+ };
186
+ const ONLINE_CAP = 30;
187
+ const onlineHandler = async (ctx) => {
188
+ const ep = dbEp(ctx);
189
+ if (!ep) return { text: "online\uFF1A\u672A\u914D\u7F6E db_host/db_port" };
190
+ try {
191
+ const st = await fetchSfmcStatus(ep);
192
+ const list = Array.isArray(st.online) ? st.online : [];
193
+ if (list.length === 0) {
194
+ return { text: st.note || "\u6682\u65E0\u5728\u7EBF\u73A9\u5BB6" };
195
+ }
196
+ const shown = list.slice(0, ONLINE_CAP);
197
+ const names = shown.map((p, i) => `${i + 1}. ${p.name || p.id || "?"}`).join("\n");
198
+ const more = list.length > ONLINE_CAP ? `
199
+ \u2026\u53E6\u6709 ${list.length - ONLINE_CAP} \u4EBA\u672A\u5217\u51FA` : "";
200
+ return { text: `\u5728\u7EBF ${list.length} \u4EBA
201
+ ${names}${more}` };
202
+ } catch (e) {
203
+ return { text: `online \u5931\u8D25\uFF1A${e.message}` };
204
+ }
205
+ };
206
+ const bindHandler = async (ctx) => {
207
+ const ep = dbEp(ctx);
208
+ if (!ep) return { text: "\u7ED1\u5B9A\uFF1A\u672A\u914D\u7F6E db_host/db_port" };
209
+ try {
210
+ const { status, data } = await postBindRequest(ep, {
211
+ openid: ctx.inbound.userId,
212
+ qq_backend: ctx.inbound.backend
213
+ });
214
+ if (status === 409 || data.error === "already_bound") {
215
+ return {
216
+ text: `\u4F60\u5DF2\u7ED1\u5B9A MC \u73A9\u5BB6\u300C${data.player_name || "?"}\u300D\u3002\u5982\u9700\u6362\u7ED1\u8BF7\u5148\u53D1\u9001\u300C\u89E3\u7ED1\u300D\u3002`
217
+ };
218
+ }
219
+ if (!data.success || !data.code) {
220
+ return { text: `\u7533\u8BF7\u7ED1\u5B9A\u5931\u8D25\uFF1A${data.error || `HTTP ${status}`}` };
221
+ }
222
+ return {
223
+ text: `\u7ED1\u5B9A\u7801\uFF1A${data.code}
224
+ \u8BF7\u5728\u6E38\u620F\u5185\u8F93\u5165 !bind\uFF0C\u7136\u540E\u5728 60 \u79D2\u5185\u53D1\u9001\u6B64\u6570\u5B57\u7801\uFF08\u4E0D\u8981\u5E26 !\uFF09\u3002`,
225
+ markdown: `## \u7ED1\u5B9A\u7801 \`${data.code}\`
226
+
227
+ 1. \u6E38\u620F\u5185\u6267\u884C **!bind**
228
+ 2. \u5728 **60 \u79D2\u5185**\u53D1\u9001\u672C\u6570\u5B57\u7801\uFF08\u4E0D\u8981\u5E26 \`!\`\uFF09
229
+
230
+ _\u8D85\u65F6\u8BF7\u91CD\u65B0\u7533\u8BF7_`
231
+ };
232
+ } catch (e) {
233
+ return { text: `\u7ED1\u5B9A\u5931\u8D25\uFF1A${e.message}` };
234
+ }
235
+ };
236
+ const unbindHandler = async (ctx) => {
237
+ const ep = dbEp(ctx);
238
+ if (!ep) return { text: "\u89E3\u7ED1\uFF1A\u672A\u914D\u7F6E db_host/db_port" };
239
+ try {
240
+ const { data } = await postBindUnbind(ep, { openid: ctx.inbound.userId });
241
+ if (data.unbound) return { text: "\u5DF2\u89E3\u9664 QQ\u2194MC \u7ED1\u5B9A\u3002" };
242
+ return { text: "\u5F53\u524D\u6CA1\u6709\u7ED1\u5B9A\u8BB0\u5F55\u3002" };
243
+ } catch (e) {
244
+ return { text: `\u89E3\u7ED1\u5931\u8D25\uFF1A${e.message}` };
245
+ }
246
+ };
247
+ const groupInfoHandler = async (ctx) => {
248
+ const lines = await fetchQqGroupLines(ctx);
249
+ if (lines.length === 0) {
250
+ return { text: "\u7FA4\u4FE1\u606F\u4EC5\u5B98\u65B9\u540E\u7AEF\u53EF\u7528\uFF0C\u4E14\u9700\u914D\u7F6E qq_group_openid / \u51ED\u8BC1\u3002" };
251
+ }
252
+ return { text: ["QQ \u7FA4\u4FE1\u606F", ...lines].join("\n") };
253
+ };
254
+ const joinHandler = async (ctx) => {
255
+ const ep = dbEp(ctx);
256
+ if (!ep) return { text: "\u7533\u8BF7\u5165\u670D\uFF1A\u672A\u914D\u7F6E db_host/db_port" };
257
+ let playerName = ctx.inbound.text.replace(/^\/?(申请入服|join)\s*/i, "").trim();
258
+ if (!playerName) {
259
+ try {
260
+ const me = await fetchBindMe(ep, ctx.inbound.userId);
261
+ if (me.bound && me.binding?.player_name) playerName = String(me.binding.player_name);
262
+ } catch {
263
+ }
264
+ }
265
+ if (!playerName) {
266
+ return { text: "\u7528\u6CD5\uFF1A\u7533\u8BF7\u5165\u670D <\u73A9\u5BB6\u540D>\n\uFF08\u5DF2\u7ED1\u5B9A\u53EF\u7701\u7565\u540D\u5B57\uFF09" };
267
+ }
268
+ try {
269
+ const { data } = await postJoinRequest(ep, {
270
+ openid: ctx.inbound.userId,
271
+ player_name: playerName,
272
+ qq_backend: ctx.inbound.backend
273
+ });
274
+ if (!data.success || !data.id) {
275
+ if (data.error === "join_allowlist_disabled") {
276
+ return { text: "\u5165\u670D\u767D\u540D\u5355\u5DF2\u5173\u95ED\uFF08\u7BA1\u7406\u5458\u53EF\u53D1\u300C\u914D\u7F6E \u767D\u540D\u5355 \u5F00\u300D\uFF09" };
277
+ }
278
+ return { text: `\u7533\u8BF7\u5931\u8D25\uFF1A${data.error || "unknown"}` };
279
+ }
280
+ if (data.auto_approved || data.status === "approved") {
281
+ return {
282
+ text: data.note ? `\u300C${playerName}\u300D${data.note}` : `\u5DF2\u81EA\u52A8\u901A\u8FC7\u300C${playerName}\u300D\u5165\u670D\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355`
283
+ };
284
+ }
285
+ const admins = ctx.runtimeInfo.adminOpenids ?? [];
286
+ const creds = ctx.runtimeInfo.officialCreds;
287
+ const gid = ctx.runtimeInfo.groupOpenid || ctx.inbound.groupId;
288
+ if (creds && gid && admins.length > 0 && ctx.inbound.backend === "official") {
289
+ void sendGroupMessage(creds, {
290
+ groupOpenid: gid,
291
+ msgType: 2,
292
+ markdown: `## \u5165\u670D\u7533\u8BF7
293
+
294
+ \u73A9\u5BB6 **${playerName}**
295
+ \u7533\u8BF7\u4EBA \`${ctx.inbound.userId.slice(0, 8)}\u2026\`
296
+ \u8BF7\u6C42 id=\`${data.id}\``,
297
+ keyboardButtons: [
298
+ {
299
+ id: `ap_${data.id}`,
300
+ label: "\u901A\u8FC7",
301
+ data: `join:approve:${data.id}`,
302
+ actionType: 1,
303
+ style: 1,
304
+ permission: { type: 0, specify_user_ids: admins }
305
+ },
306
+ {
307
+ id: `rj_${data.id}`,
308
+ label: "\u62D2\u7EDD",
309
+ data: `join:reject:${data.id}`,
310
+ actionType: 1,
311
+ style: 0,
312
+ permission: { type: 0, specify_user_ids: admins }
313
+ }
314
+ ]
315
+ }).catch(() => void 0);
316
+ }
317
+ const note = data.note ? `
318
+ ${data.note}` : admins.length === 0 ? "\n\u672A\u914D\u7F6E qq_admin_openids\uFF0C\u7BA1\u7406\u5458\u6536\u4E0D\u5230\u901A\u77E5" : "";
319
+ return {
320
+ text: `\u5DF2\u63D0\u4EA4\u5165\u670D\u7533\u8BF7\u300C${playerName}\u300D${note}`,
321
+ buttons: ctx.inbound.backend === "llbot" && admins.length > 0 ? [
322
+ { id: "a", label: `\u901A\u8FC7 ${playerName}`, command: `/approve ${data.id}` },
323
+ { id: "r", label: `\u62D2\u7EDD ${playerName}`, command: `/reject ${data.id}` }
324
+ ] : void 0
325
+ };
326
+ } catch (e) {
327
+ return { text: `\u7533\u8BF7\u5931\u8D25\uFF1A${e.message}` };
328
+ }
329
+ };
330
+ function parseOnOff(raw) {
331
+ const s = raw.trim().toLowerCase();
332
+ if (["\u5F00", "\u5F00\u542F", "on", "true", "1", "yes"].includes(s)) return true;
333
+ if (["\u5173", "\u5173\u95ED", "off", "false", "0", "no"].includes(s)) return false;
334
+ return null;
335
+ }
336
+ const joinConfigHandler = async (ctx) => {
337
+ if (!isAdmin(ctx)) return { text: "\u4EC5\u7BA1\u7406\u5458\u53EF\u914D\u7F6E\u5165\u670D\u5F00\u5173\uFF08qq_admin_openids / \u7FA4\u7BA1\u89C6\u4F5C\u7BA1\u7406\u5458\uFF09" };
338
+ const ep = dbEp(ctx);
339
+ if (!ep) return { text: "\u914D\u7F6E\uFF1A\u672A\u8FDE\u63A5 db" };
340
+ const admins = ctx.runtimeInfo.adminOpenids ?? [];
341
+ const rest = ctx.inbound.text.replace(/^\/?(配置|config|入服配置)\s*/i, "").trim();
342
+ if (!rest) {
343
+ try {
344
+ const data = await fetchJoinSettings(ep);
345
+ if (data.success === false) return { text: `\u8BFB\u53D6\u5931\u8D25\uFF1A${data.error || "unknown"}` };
346
+ return buildJoinSettingsPanel({
347
+ settings: settingsFromResponse(data),
348
+ backend: ctx.inbound.backend,
349
+ adminOpenids: admins
350
+ });
351
+ } catch (e) {
352
+ return { text: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
353
+ }
354
+ }
355
+ if (/^(群管|群管理员|treat|group.?admin)/i.test(rest)) {
356
+ return {
357
+ text: "\u300C\u7FA4\u7BA1\u89C6\u4F5C\u7BA1\u7406\u5458\u300D\u53EA\u80FD\u6539 configs/qq_link.json \u7684 treat_group_admins_as_admins\uFF0C\u7FA4\u804A\u4E0D\u53EF\u6539\u3002\u53D1\u300C\u914D\u7F6E\u300D\u53EF\u67E5\u770B\u5F53\u524D\u503C\u3002"
358
+ };
359
+ }
360
+ const m = /^(白名单|allowlist|审批|approval)\s+(\S+)/i.exec(rest);
361
+ if (!m) {
362
+ return { text: "\u7528\u6CD5\uFF1A\u914D\u7F6E\n\u3000\u3000\u3000\u6216\u70B9\u9762\u677F\u6309\u94AE\u5207\u6362\u767D\u540D\u5355/\u5BA1\u6279" };
363
+ }
364
+ const which = m[1].toLowerCase();
365
+ const onOff = parseOnOff(m[2]);
366
+ if (onOff === null) return { text: "\u8BF7\u4F7F\u7528 \u5F00/\u5173\uFF08\u6216 on/off\uFF09" };
367
+ const body = {
368
+ openid: ctx.inbound.userId,
369
+ as_group_admin: asGroupAdminField(ctx)
370
+ };
371
+ if (which === "\u767D\u540D\u5355" || which === "allowlist") body.allowlist_enabled = onOff;
372
+ else body.require_approval = onOff;
373
+ try {
374
+ const { data } = await postJoinSettings(ep, body);
375
+ if (data.success === false) {
376
+ if (data.error === "not_admin") {
377
+ return { text: "\u65E0\u6743\u9650\uFF08\u7FA4\u7BA1\u9700 configs/qq_link.json \u4E2D treat_group_admins_as_admins=true\uFF09" };
378
+ }
379
+ if (data.error === "immutable_field") {
380
+ return { text: data.note || "\u8BE5\u5B57\u6BB5\u4E0D\u53EF\u7ECF\u7FA4\u804A\u4FEE\u6539" };
381
+ }
382
+ return { text: `\u4FDD\u5B58\u5931\u8D25\uFF1A${data.error || "unknown"}` };
383
+ }
384
+ return buildJoinSettingsPanel({
385
+ settings: settingsFromResponse(data),
386
+ backend: ctx.inbound.backend,
387
+ adminOpenids: admins,
388
+ prefix: "\u5DF2\u4FDD\u5B58"
389
+ });
390
+ } catch (e) {
391
+ return { text: `\u4FDD\u5B58\u5931\u8D25\uFF1A${e.message}` };
392
+ }
393
+ };
394
+ const pendingHandler = async (ctx) => {
395
+ if (!isAdmin(ctx)) return { text: "\u4EC5\u7BA1\u7406\u5458\u53EF\u67E5\u770B\u5F85\u5BA1\u5217\u8868\uFF08\u914D\u7F6E qq_admin_openids\uFF09" };
396
+ const ep = dbEp(ctx);
397
+ if (!ep) return { text: "\u5F85\u5BA1\uFF1A\u672A\u914D\u7F6E db" };
398
+ try {
399
+ const data = await fetchJoinPending(ep, ctx.inbound.userId, asGroupAdminField(ctx));
400
+ if (data.error) {
401
+ if (data.error === "not_admin") {
402
+ return { text: "\u65E0\u6743\u9650\uFF08\u7FA4\u7BA1\u9700 treat_group_admins_as_admins=true\uFF09" };
403
+ }
404
+ return { text: `\u5F85\u5BA1\u5931\u8D25\uFF1A${data.error}` };
405
+ }
406
+ const list = data.pending ?? [];
407
+ if (list.length === 0) return { text: "\u6682\u65E0\u5F85\u5BA1\u5165\u670D\u7533\u8BF7" };
408
+ const lines = list.map((r, i) => `${i + 1}. ${r.player_name || "?"} (${r.id})`);
409
+ return { text: `\u5F85\u5BA1 ${list.length} \u6761
410
+ ${lines.join("\n")}` };
411
+ } catch (e) {
412
+ return { text: `\u5F85\u5BA1\u5931\u8D25\uFF1A${e.message}` };
413
+ }
414
+ };
415
+ const approveHandler = async (ctx) => {
416
+ if (!isAdmin(ctx)) return { text: "\u4EC5\u7BA1\u7406\u5458\u53EF\u5BA1\u6279" };
417
+ const ep = dbEp(ctx);
418
+ if (!ep) return { text: "\u672A\u914D\u7F6E db" };
419
+ const id = ctx.inbound.text.replace(/^\/?(approve|通过)\s*/i, "").trim();
420
+ if (!id) return { text: "\u7528\u6CD5\uFF1A\u901A\u8FC7 <\u7533\u8BF7id>" };
421
+ try {
422
+ const { data } = await postJoinDecide(ep, {
423
+ id,
424
+ decision: "approve",
425
+ decided_by: ctx.inbound.userId,
426
+ as_group_admin: asGroupAdminField(ctx)
427
+ });
428
+ if (data.success === false && data.error === "not_admin") {
429
+ return { text: "\u65E0\u6743\u9650\uFF08\u7FA4\u7BA1\u9700 treat_group_admins_as_admins=true\uFF09" };
430
+ }
431
+ return { text: data.success === false ? `\u5931\u8D25\uFF1A${data.error}` : `\u5DF2\u901A\u8FC7\u300C${data.player_name || id}\u300D` };
432
+ } catch (e) {
433
+ return { text: `\u5931\u8D25\uFF1A${e.message}` };
434
+ }
435
+ };
436
+ const rejectHandler = async (ctx) => {
437
+ if (!isAdmin(ctx)) return { text: "\u4EC5\u7BA1\u7406\u5458\u53EF\u5BA1\u6279" };
438
+ const ep = dbEp(ctx);
439
+ if (!ep) return { text: "\u672A\u914D\u7F6E db" };
440
+ const id = ctx.inbound.text.replace(/^\/?(reject|拒绝)\s*/i, "").trim();
441
+ if (!id) return { text: "\u7528\u6CD5\uFF1A\u62D2\u7EDD <\u7533\u8BF7id>" };
442
+ try {
443
+ const { data } = await postJoinDecide(ep, {
444
+ id,
445
+ decision: "reject",
446
+ decided_by: ctx.inbound.userId,
447
+ as_group_admin: asGroupAdminField(ctx)
448
+ });
449
+ if (data.success === false && data.error === "not_admin") {
450
+ return { text: "\u65E0\u6743\u9650\uFF08\u7FA4\u7BA1\u9700 treat_group_admins_as_admins=true\uFF09" };
451
+ }
452
+ return { text: data.success === false ? `\u5931\u8D25\uFF1A${data.error}` : `\u5DF2\u62D2\u7EDD\u300C${data.player_name || id}\u300D` };
453
+ } catch (e) {
454
+ return { text: `\u5931\u8D25\uFF1A${e.message}` };
455
+ }
456
+ };
457
+ const channelHandler = async (ctx) => {
458
+ const id = String(ctx.runtimeInfo.bridgeChannelId ?? "").trim();
459
+ const lines = [
460
+ "\u3010\u804A\u5929\u4E92\u901A\u3011",
461
+ id ? `\u9891\u9053\uFF1A${id}` : "\u9891\u9053\uFF1A\u672A\u914D\u7F6E\uFF08\u8BF7\u8BBE qq_config.json \u2192 bridge_channel_id \u5E76\u91CD\u542F db/qq/BDS\uFF09",
462
+ `\u540E\u7AEF\uFF1A${ctx.inbound.backend}${ctx.runtimeInfo.sandbox ? " \xB7 sandbox" : ""}`
463
+ ];
464
+ const ep = dbEp(ctx);
465
+ if (!ep) {
466
+ lines.push("db\uFF1A\u672A\u914D\u7F6E db_host/db_port");
467
+ } else {
468
+ try {
469
+ const st = await fetchSfmcStatus(ep);
470
+ const bds = st.processes?.bds;
471
+ const db = st.processes?.db;
472
+ lines.push(`db-server\uFF1A${db?.state === "stopped" || db?.running === false ? "\u672A\u8FD0\u884C" : db?.uptimeText || "\u53EF\u8FBE"}${db?.pid ? ` (PID ${db.pid})` : ""}`);
473
+ lines.push(
474
+ `BDS\uFF1A${bds?.state === "running" || bds?.running === true ? `\u8FD0\u884C\u4E2D \xB7 ${bds.uptimeText || "\u2014"}` : "\u672A\u8FD0\u884C"}`
475
+ );
476
+ } catch (e) {
477
+ lines.push(`db\uFF1A\u4E0D\u53EF\u8FBE\uFF08${e.message}\uFF09`);
478
+ }
479
+ }
480
+ lines.push("\u63D0\u793A\uFF1A\u5B98\u65B9\u4EC5 @\u673A\u5668\u4EBA \u7684\u975E\u6307\u4EE4\u6D88\u606F\u8FDB\u6E38\u620F\uFF1B\u6E38\u620F\u804A\u5929\u63A8\u7FA4\u8D70 MC\u2192QQ\u3002");
481
+ const text = lines.join("\n");
482
+ return {
483
+ text,
484
+ markdown: text.split("\n").map((l, i) => i === 0 ? `## ${l.replace(/【|】/g, "")}` : `- ${l}`).join("\n")
485
+ };
486
+ };
487
+ const doctorHandler = async (ctx) => {
488
+ if (!isAdmin(ctx)) return { text: "\u4EC5\u7BA1\u7406\u5458\u53EF\u6267\u884C\u81EA\u68C0" };
489
+ const base = await channelHandler(ctx);
490
+ const extra = ["", "\u3010\u7BA1\u7406\u81EA\u68C0\u3011"];
491
+ const admins = ctx.runtimeInfo.adminOpenids ?? [];
492
+ extra.push(`\u7BA1\u7406\u5458 openid \u6570\uFF1A${admins.length}${admins.length === 0 ? "\uFF08\u7A7A\u5219\u65E0\u6CD5\u5BA1\u6279/\u8E22\u4EBA\uFF09" : ""}`);
493
+ extra.push(`\u7FA4 openid\uFF1A${ctx.runtimeInfo.groupOpenid ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
494
+ if (ctx.inbound.backend === "official") {
495
+ extra.push(`\u51ED\u8BC1\uFF1A${ctx.runtimeInfo.officialCreds ? "\u5DF2\u6CE8\u5165" : "\u7F3A\u5931"}`);
496
+ }
497
+ const text = `${base.text}
498
+ ${extra.join("\n")}`;
499
+ return { text, markdown: `${base.markdown ?? base.text}
500
+
501
+ ### \u7BA1\u7406\u81EA\u68C0
502
+
503
+ ${extra.filter(Boolean).map((l) => `- ${l}`).join("\n")}` };
504
+ };
505
+ const kickHandler = async (ctx) => {
506
+ if (!isAdmin(ctx)) return { text: "\u4EC5\u7BA1\u7406\u5458\u53EF\u8E22\u4EBA\uFF08\u914D\u7F6E qq_admin_openids\uFF09" };
507
+ const ep = dbEp(ctx);
508
+ if (!ep) return { text: "\u8E22\u4EBA\uFF1A\u672A\u914D\u7F6E db" };
509
+ const target = ctx.inbound.text.replace(/^\/?(踢人|kick)\s*/i, "").trim();
510
+ if (!target) return { text: "\u7528\u6CD5\uFF1A\u8E22\u4EBA <\u73A9\u5BB6\u540D>" };
511
+ try {
512
+ const { data } = await postAdminKick(ep, {
513
+ openid: ctx.inbound.userId,
514
+ target_name: target,
515
+ reason: "QQ \u7BA1\u7406\u5458\u8E22\u51FA",
516
+ as_group_admin: asGroupAdminField(ctx)
517
+ });
518
+ if (!data.success) {
519
+ if (data.error === "not_admin") {
520
+ return { text: "\u65E0\u6743\u9650\uFF08\u7FA4\u7BA1\u9700 treat_group_admins_as_admins=true\uFF09" };
521
+ }
522
+ return { text: `\u8E22\u4EBA\u5165\u961F\u5931\u8D25\uFF1A${data.error || "unknown"}` };
523
+ }
524
+ return { text: `\u5DF2\u5C06\u300C${target}\u300D\u8E22\u4EBA\u8BF7\u6C42\u5165\u961F\uFF0C\u7B49\u5F85 BDS \u6267\u884C\u3002` };
525
+ } catch (e) {
526
+ return { text: `\u8E22\u4EBA\u5931\u8D25\uFF1A${e.message}` };
527
+ }
528
+ };
529
+ function registerBuiltinCommands(registry) {
530
+ registry.register({
531
+ name: "menu",
532
+ aliases: ["\u83DC\u5355", "help", "\u5E2E\u52A9", "/help", "/\u83DC\u5355", "/menu"],
533
+ description: "\u663E\u793A\u6307\u4EE4\u83DC\u5355",
534
+ handler: createMenuHandler(registry)
535
+ });
536
+ registry.register({
537
+ name: "ping",
538
+ aliases: ["/ping"],
539
+ description: "\u8FDE\u901A\u6027\u63A2\u6D4B",
540
+ handler: pingHandler
541
+ });
542
+ registry.register({
543
+ name: "whoami",
544
+ aliases: ["/whoami", "\u6211\u7684\u7ED1\u5B9A"],
545
+ description: "\u67E5\u770B QQ id \u4E0E MC \u7ED1\u5B9A",
546
+ handler: whoamiHandler
547
+ });
548
+ registry.register({
549
+ name: "status",
550
+ aliases: ["\u72B6\u6001", "/status", "/\u72B6\u6001"],
551
+ description: "\u670D\u52A1\u5668\u72B6\u6001\u6458\u8981",
552
+ handler: statusHandler
553
+ });
554
+ registry.register({
555
+ name: "online",
556
+ aliases: ["\u5728\u7EBF", "/online", "/\u5728\u7EBF"],
557
+ description: "\u5728\u7EBF\u73A9\u5BB6\u540D\u5355",
558
+ handler: onlineHandler
559
+ });
560
+ registry.register({
561
+ name: "bind",
562
+ aliases: ["\u7ED1\u5B9A", "/bind", "/\u7ED1\u5B9A"],
563
+ description: "\u7533\u8BF7 QQ\u2194MC \u7ED1\u5B9A\u7801",
564
+ handler: bindHandler
565
+ });
566
+ registry.register({
567
+ name: "unbind",
568
+ aliases: ["\u89E3\u7ED1", "/unbind", "/\u89E3\u7ED1"],
569
+ description: "\u89E3\u9664 QQ\u2194MC \u7ED1\u5B9A",
570
+ handler: unbindHandler
571
+ });
572
+ registry.register({
573
+ name: "join",
574
+ aliases: ["\u7533\u8BF7\u5165\u670D", "/join", "/\u7533\u8BF7\u5165\u670D"],
575
+ description: "\u7533\u8BF7\u52A0\u5165\u670D\u52A1\u5668\u767D\u540D\u5355",
576
+ handler: joinHandler
577
+ });
578
+ registry.register({
579
+ name: "channel",
580
+ aliases: ["\u9891\u9053", "/channel", "/\u9891\u9053"],
581
+ description: "\u804A\u5929\u4E92\u901A\u9891\u9053\u4E0E\u8FDE\u901A\u81EA\u68C0",
582
+ handler: channelHandler
583
+ });
584
+ registry.register({
585
+ name: "admin",
586
+ aliases: ["\u7BA1\u7406", "/admin", "/\u7BA1\u7406"],
587
+ description: "\u7BA1\u7406\u5B50\u83DC\u5355\uFF08\u7BA1\u7406\u5458\uFF09",
588
+ handler: createAdminMenuHandler(registry)
589
+ });
590
+ registry.register({
591
+ name: "doctor",
592
+ aliases: ["\u81EA\u68C0", "/doctor", "/\u81EA\u68C0"],
593
+ description: "\u7BA1\u7406\u4FA7\u8FDE\u901A/\u914D\u7F6E\u81EA\u68C0",
594
+ handler: doctorHandler,
595
+ adminMenu: true
596
+ });
597
+ registry.register({
598
+ name: "group",
599
+ aliases: ["\u7FA4\u4FE1\u606F", "/group", "/\u7FA4\u4FE1\u606F"],
600
+ description: "QQ \u7FA4 OpenAPI \u6458\u8981",
601
+ handler: groupInfoHandler,
602
+ adminMenu: true
603
+ });
604
+ registry.register({
605
+ name: "config",
606
+ aliases: ["\u914D\u7F6E", "\u5165\u670D\u914D\u7F6E", "/config", "/\u914D\u7F6E"],
607
+ description: "\u5165\u670D\u767D\u540D\u5355/\u5BA1\u6279\u5F00\u5173\uFF08\u7BA1\u7406\u5458\uFF09",
608
+ handler: joinConfigHandler,
609
+ adminMenu: true
610
+ });
611
+ registry.register({
612
+ name: "pending",
613
+ aliases: ["\u5F85\u5BA1", "/pending", "/\u5F85\u5BA1"],
614
+ description: "\u5F85\u5BA1\u5165\u670D\u5217\u8868\uFF08\u7BA1\u7406\u5458\uFF09",
615
+ handler: pendingHandler,
616
+ adminMenu: true
617
+ });
618
+ registry.register({
619
+ name: "approve",
620
+ aliases: ["\u901A\u8FC7", "/approve", "/\u901A\u8FC7"],
621
+ description: "\u901A\u8FC7\u5165\u670D\u7533\u8BF7\uFF08\u7BA1\u7406\u5458\uFF09",
622
+ handler: approveHandler,
623
+ adminMenu: true
624
+ });
625
+ registry.register({
626
+ name: "reject",
627
+ aliases: ["\u62D2\u7EDD", "/reject", "/\u62D2\u7EDD"],
628
+ description: "\u62D2\u7EDD\u5165\u670D\u7533\u8BF7\uFF08\u7BA1\u7406\u5458\uFF09",
629
+ handler: rejectHandler,
630
+ adminMenu: true
631
+ });
632
+ registry.register({
633
+ name: "kick",
634
+ aliases: ["\u8E22\u4EBA", "/kick", "/\u8E22\u4EBA"],
635
+ description: "\u8E22\u51FA\u6E38\u620F\u5185\u73A9\u5BB6\uFF08\u7BA1\u7406\u5458\uFF09",
636
+ handler: kickHandler,
637
+ adminMenu: true
638
+ });
639
+ }
640
+ export {
641
+ approveHandler,
642
+ bindHandler,
643
+ channelHandler,
644
+ createAdminMenuHandler,
645
+ createMenuHandler,
646
+ doctorHandler,
647
+ groupInfoHandler,
648
+ joinConfigHandler,
649
+ joinHandler,
650
+ kickHandler,
651
+ onlineHandler,
652
+ pendingHandler,
653
+ pingHandler,
654
+ registerBuiltinCommands,
655
+ rejectHandler,
656
+ statusHandler,
657
+ unbindHandler,
658
+ whoamiHandler
659
+ };
660
+ //# sourceMappingURL=handlers.js.map