@hyl_aa/openclaw-napcat 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 +21 -0
- package/README.md +129 -0
- package/README_EN.md +129 -0
- package/index.ts +17 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +49 -0
- package/src/accounts.ts +101 -0
- package/src/api.ts +134 -0
- package/src/channel.ts +366 -0
- package/src/config-schema.ts +72 -0
- package/src/monitor.ts +565 -0
- package/src/probe.ts +37 -0
- package/src/runtime.ts +6 -0
- package/src/send.ts +95 -0
- package/src/tools.ts +635 -0
- package/src/types.ts +104 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAccountScopedDmSecurityPolicy,
|
|
3
|
+
buildChannelSendResult,
|
|
4
|
+
buildBaseAccountStatusSnapshot,
|
|
5
|
+
buildTokenChannelStatusSummary,
|
|
6
|
+
DEFAULT_ACCOUNT_ID,
|
|
7
|
+
deleteAccountFromConfigSection,
|
|
8
|
+
setAccountEnabledInConfigSection,
|
|
9
|
+
applyAccountNameToChannelSection,
|
|
10
|
+
applySetupAccountConfigPatch,
|
|
11
|
+
migrateBaseNameToDefaultAccount,
|
|
12
|
+
chunkTextForOutbound,
|
|
13
|
+
formatAllowFromLowercase,
|
|
14
|
+
isNumericTargetId,
|
|
15
|
+
listDirectoryUserEntriesFromAllowFrom,
|
|
16
|
+
mapAllowFromEntries,
|
|
17
|
+
normalizeAccountId,
|
|
18
|
+
sendPayloadWithChunkedTextAndMedia,
|
|
19
|
+
collectOpenProviderGroupPolicyWarnings,
|
|
20
|
+
buildOpenGroupPolicyRestrictSendersWarning,
|
|
21
|
+
buildOpenGroupPolicyWarning,
|
|
22
|
+
PAIRING_APPROVED_MESSAGE,
|
|
23
|
+
} from "openclaw/plugin-sdk";
|
|
24
|
+
import type {
|
|
25
|
+
ChannelAccountSnapshot,
|
|
26
|
+
ChannelDock,
|
|
27
|
+
ChannelPlugin,
|
|
28
|
+
OpenClawConfig,
|
|
29
|
+
} from "openclaw/plugin-sdk";
|
|
30
|
+
// Inline — avoids SDK export resolution issues at runtime.
|
|
31
|
+
function createAccountStatusSink(params: {
|
|
32
|
+
accountId: string;
|
|
33
|
+
setStatus: (next: Record<string, unknown>) => void;
|
|
34
|
+
}): (patch: Record<string, unknown>) => void {
|
|
35
|
+
return (patch) => {
|
|
36
|
+
params.setStatus({ accountId: params.accountId, ...patch });
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
listNapCatAccountIds,
|
|
42
|
+
resolveDefaultNapCatAccountId,
|
|
43
|
+
resolveNapCatAccount,
|
|
44
|
+
type ResolvedNapCatAccount,
|
|
45
|
+
} from "./accounts.js";
|
|
46
|
+
import { NapCatChannelConfigSchema } from "./config-schema.js";
|
|
47
|
+
import { createNapCatAgentTools } from "./tools.js";
|
|
48
|
+
import { probeNapCat } from "./probe.js";
|
|
49
|
+
import { sendMessageNapCat } from "./send.js";
|
|
50
|
+
import { sendPrivateMsg, textSegment } from "./api.js";
|
|
51
|
+
|
|
52
|
+
const meta = {
|
|
53
|
+
id: "napcat" as const,
|
|
54
|
+
label: "QQ (NapCat)",
|
|
55
|
+
selectionLabel: "QQ via NapCat (OneBot 11)",
|
|
56
|
+
docsPath: "/channels/napcat",
|
|
57
|
+
docsLabel: "napcat",
|
|
58
|
+
blurb: "QQ messaging via NapCat OneBot 11 reverse WebSocket.",
|
|
59
|
+
aliases: ["qq", "onebot"],
|
|
60
|
+
order: 90,
|
|
61
|
+
quickstartAllowFrom: true,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function normalizeNapCatMessagingTarget(raw: string): string | undefined {
|
|
65
|
+
const trimmed = raw?.trim();
|
|
66
|
+
if (!trimmed) return undefined;
|
|
67
|
+
// Strip prefixes like "napcat:", "qq:", "onebot:"
|
|
68
|
+
return trimmed.replace(/^(napcat|qq|onebot):/i, "");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const napCatDock: ChannelDock = {
|
|
72
|
+
id: "napcat",
|
|
73
|
+
capabilities: {
|
|
74
|
+
chatTypes: ["direct", "group"],
|
|
75
|
+
media: true,
|
|
76
|
+
blockStreaming: true,
|
|
77
|
+
},
|
|
78
|
+
outbound: { textChunkLimit: 4500 },
|
|
79
|
+
config: {
|
|
80
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
81
|
+
mapAllowFromEntries(resolveNapCatAccount({ cfg, accountId }).config.allowFrom),
|
|
82
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
83
|
+
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(napcat|qq|onebot):/i }),
|
|
84
|
+
},
|
|
85
|
+
groups: {
|
|
86
|
+
resolveRequireMention: () => true,
|
|
87
|
+
},
|
|
88
|
+
threading: {
|
|
89
|
+
resolveReplyToMode: () => "off",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const napCatPlugin: ChannelPlugin<ResolvedNapCatAccount> = {
|
|
94
|
+
id: "napcat",
|
|
95
|
+
meta,
|
|
96
|
+
capabilities: {
|
|
97
|
+
chatTypes: ["direct", "group"],
|
|
98
|
+
media: true,
|
|
99
|
+
reactions: false,
|
|
100
|
+
threads: false,
|
|
101
|
+
polls: false,
|
|
102
|
+
nativeCommands: false,
|
|
103
|
+
blockStreaming: true,
|
|
104
|
+
},
|
|
105
|
+
reload: { configPrefixes: ["channels.napcat"] },
|
|
106
|
+
configSchema: NapCatChannelConfigSchema,
|
|
107
|
+
agentTools: ({ cfg }) => (cfg ? createNapCatAgentTools(cfg) : []),
|
|
108
|
+
config: {
|
|
109
|
+
listAccountIds: (cfg) => listNapCatAccountIds(cfg),
|
|
110
|
+
resolveAccount: (cfg, accountId) => resolveNapCatAccount({ cfg, accountId }),
|
|
111
|
+
defaultAccountId: (cfg) => resolveDefaultNapCatAccountId(cfg),
|
|
112
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
113
|
+
setAccountEnabledInConfigSection({
|
|
114
|
+
cfg,
|
|
115
|
+
sectionKey: "napcat",
|
|
116
|
+
accountId,
|
|
117
|
+
enabled,
|
|
118
|
+
allowTopLevel: true,
|
|
119
|
+
}),
|
|
120
|
+
deleteAccount: ({ cfg, accountId }) =>
|
|
121
|
+
deleteAccountFromConfigSection({
|
|
122
|
+
cfg,
|
|
123
|
+
sectionKey: "napcat",
|
|
124
|
+
accountId,
|
|
125
|
+
clearBaseFields: ["httpApi", "accessToken", "selfId", "name"],
|
|
126
|
+
}),
|
|
127
|
+
isConfigured: (account) => Boolean(account.httpApi?.trim()),
|
|
128
|
+
describeAccount: (account): ChannelAccountSnapshot => ({
|
|
129
|
+
accountId: account.accountId,
|
|
130
|
+
name: account.name,
|
|
131
|
+
enabled: account.enabled,
|
|
132
|
+
configured: Boolean(account.httpApi?.trim()),
|
|
133
|
+
}),
|
|
134
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
135
|
+
mapAllowFromEntries(resolveNapCatAccount({ cfg, accountId }).config.allowFrom),
|
|
136
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
137
|
+
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(napcat|qq|onebot):/i }),
|
|
138
|
+
},
|
|
139
|
+
security: {
|
|
140
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
141
|
+
return buildAccountScopedDmSecurityPolicy({
|
|
142
|
+
cfg,
|
|
143
|
+
channelKey: "napcat",
|
|
144
|
+
accountId,
|
|
145
|
+
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
146
|
+
policy: account.config.dmPolicy,
|
|
147
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
148
|
+
policyPathSuffix: "dmPolicy",
|
|
149
|
+
normalizeEntry: (raw) => raw.replace(/^(napcat|qq|onebot):/i, ""),
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
collectWarnings: ({ account, cfg }) => {
|
|
153
|
+
return collectOpenProviderGroupPolicyWarnings({
|
|
154
|
+
cfg,
|
|
155
|
+
providerConfigPresent: (cfg as Record<string, unknown>).channels?.napcat !== undefined,
|
|
156
|
+
configuredGroupPolicy: account.config.groupPolicy,
|
|
157
|
+
collect: (groupPolicy) => {
|
|
158
|
+
if (groupPolicy !== "open") return [];
|
|
159
|
+
const explicitGroupAllowFrom = mapAllowFromEntries(account.config.groupAllowFrom);
|
|
160
|
+
const dmAllowFrom = mapAllowFromEntries(account.config.allowFrom);
|
|
161
|
+
const effectiveAllowFrom =
|
|
162
|
+
explicitGroupAllowFrom.length > 0 ? explicitGroupAllowFrom : dmAllowFrom;
|
|
163
|
+
if (effectiveAllowFrom.length > 0) {
|
|
164
|
+
return [
|
|
165
|
+
buildOpenGroupPolicyRestrictSendersWarning({
|
|
166
|
+
surface: "QQ groups",
|
|
167
|
+
openScope: "any member",
|
|
168
|
+
groupPolicyPath: "channels.napcat.groupPolicy",
|
|
169
|
+
groupAllowFromPath: "channels.napcat.groupAllowFrom",
|
|
170
|
+
}),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
return [
|
|
174
|
+
buildOpenGroupPolicyWarning({
|
|
175
|
+
surface: "QQ groups",
|
|
176
|
+
openBehavior:
|
|
177
|
+
"with no groupAllowFrom/allowFrom allowlist; any member can trigger (mention-gated)",
|
|
178
|
+
remediation:
|
|
179
|
+
'Set channels.napcat.groupPolicy="allowlist" + channels.napcat.groupAllowFrom',
|
|
180
|
+
}),
|
|
181
|
+
];
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
groups: {
|
|
187
|
+
resolveRequireMention: () => true,
|
|
188
|
+
},
|
|
189
|
+
threading: {
|
|
190
|
+
resolveReplyToMode: () => "off",
|
|
191
|
+
},
|
|
192
|
+
messaging: {
|
|
193
|
+
normalizeTarget: normalizeNapCatMessagingTarget,
|
|
194
|
+
targetResolver: {
|
|
195
|
+
looksLikeId: isNumericTargetId,
|
|
196
|
+
hint: "<qqNumber> or group:<groupId>",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
directory: {
|
|
200
|
+
self: async () => null,
|
|
201
|
+
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
202
|
+
const account = resolveNapCatAccount({ cfg, accountId });
|
|
203
|
+
return listDirectoryUserEntriesFromAllowFrom({
|
|
204
|
+
allowFrom: account.config.allowFrom,
|
|
205
|
+
query,
|
|
206
|
+
limit,
|
|
207
|
+
normalizeId: (entry) => entry.replace(/^(napcat|qq|onebot):/i, ""),
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
listGroups: async () => [],
|
|
211
|
+
},
|
|
212
|
+
setup: {
|
|
213
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
214
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
215
|
+
applyAccountNameToChannelSection({
|
|
216
|
+
cfg,
|
|
217
|
+
channelKey: "napcat",
|
|
218
|
+
accountId,
|
|
219
|
+
name,
|
|
220
|
+
}),
|
|
221
|
+
validateInput: ({ input }) => {
|
|
222
|
+
if (!input.token && !input.useEnv) {
|
|
223
|
+
return "NapCat requires httpApi URL. Set channels.napcat.httpApi in config.";
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
},
|
|
227
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
228
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
229
|
+
cfg,
|
|
230
|
+
channelKey: "napcat",
|
|
231
|
+
accountId,
|
|
232
|
+
name: input.name,
|
|
233
|
+
});
|
|
234
|
+
const next =
|
|
235
|
+
accountId !== DEFAULT_ACCOUNT_ID
|
|
236
|
+
? migrateBaseNameToDefaultAccount({ cfg: namedConfig, channelKey: "napcat" })
|
|
237
|
+
: namedConfig;
|
|
238
|
+
const patch = input.token ? { httpApi: input.token } : {};
|
|
239
|
+
return applySetupAccountConfigPatch({
|
|
240
|
+
cfg: next,
|
|
241
|
+
channelKey: "napcat",
|
|
242
|
+
accountId,
|
|
243
|
+
patch,
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
pairing: {
|
|
248
|
+
idLabel: "qqNumber",
|
|
249
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(napcat|qq|onebot):/i, ""),
|
|
250
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
251
|
+
const account = resolveNapCatAccount({ cfg });
|
|
252
|
+
if (!account.httpApi) {
|
|
253
|
+
throw new Error("NapCat httpApi not configured");
|
|
254
|
+
}
|
|
255
|
+
await sendPrivateMsg(
|
|
256
|
+
account.httpApi,
|
|
257
|
+
Number(id),
|
|
258
|
+
[textSegment(PAIRING_APPROVED_MESSAGE)],
|
|
259
|
+
account.accessToken,
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
outbound: {
|
|
264
|
+
deliveryMode: "direct",
|
|
265
|
+
chunker: chunkTextForOutbound,
|
|
266
|
+
chunkerMode: "text",
|
|
267
|
+
textChunkLimit: 4500,
|
|
268
|
+
sendPayload: async (ctx) =>
|
|
269
|
+
await sendPayloadWithChunkedTextAndMedia({
|
|
270
|
+
ctx,
|
|
271
|
+
textChunkLimit: napCatPlugin.outbound!.textChunkLimit,
|
|
272
|
+
chunker: napCatPlugin.outbound!.chunker,
|
|
273
|
+
sendText: (nextCtx) => napCatPlugin.outbound!.sendText!(nextCtx),
|
|
274
|
+
sendMedia: (nextCtx) => napCatPlugin.outbound!.sendMedia!(nextCtx),
|
|
275
|
+
emptyResult: { channel: "napcat", messageId: "" },
|
|
276
|
+
}),
|
|
277
|
+
sendText: async ({ to, text, accountId, cfg }) => {
|
|
278
|
+
const result = await sendMessageNapCat(to, text, { accountId, cfg });
|
|
279
|
+
return buildChannelSendResult("napcat", result);
|
|
280
|
+
},
|
|
281
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
|
|
282
|
+
const result = await sendMessageNapCat(to, text, { accountId, cfg, mediaUrl });
|
|
283
|
+
return buildChannelSendResult("napcat", result);
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
status: {
|
|
287
|
+
defaultRuntime: {
|
|
288
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
289
|
+
running: false,
|
|
290
|
+
lastStartAt: null,
|
|
291
|
+
lastStopAt: null,
|
|
292
|
+
lastError: null,
|
|
293
|
+
},
|
|
294
|
+
collectStatusIssues: () => [],
|
|
295
|
+
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
|
|
296
|
+
probeAccount: async ({ account, timeoutMs }) =>
|
|
297
|
+
probeNapCat(account.httpApi, account.accessToken, timeoutMs),
|
|
298
|
+
buildAccountSnapshot: ({ account, runtime }) => {
|
|
299
|
+
const configured = Boolean(account.httpApi?.trim());
|
|
300
|
+
const base = buildBaseAccountStatusSnapshot({
|
|
301
|
+
account: {
|
|
302
|
+
accountId: account.accountId,
|
|
303
|
+
name: account.name,
|
|
304
|
+
enabled: account.enabled,
|
|
305
|
+
configured,
|
|
306
|
+
},
|
|
307
|
+
runtime,
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
...base,
|
|
311
|
+
mode: "reverse-ws",
|
|
312
|
+
dmPolicy: account.config.dmPolicy ?? "allowlist",
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
gateway: {
|
|
317
|
+
startAccount: async (ctx) => {
|
|
318
|
+
const account = ctx.account;
|
|
319
|
+
const httpApi = account.httpApi.trim();
|
|
320
|
+
|
|
321
|
+
let botLabel = "";
|
|
322
|
+
try {
|
|
323
|
+
const probe = await probeNapCat(httpApi, account.accessToken, 3000);
|
|
324
|
+
const name = probe.ok ? probe.bot?.nickname?.trim() : null;
|
|
325
|
+
if (name) botLabel = ` (${name}, QQ:${probe.bot?.user_id})`;
|
|
326
|
+
if (!probe.ok) {
|
|
327
|
+
ctx.log?.warn?.(
|
|
328
|
+
`[${account.accountId}] NapCat probe failed: ${probe.error}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
ctx.setStatus({
|
|
332
|
+
accountId: account.accountId,
|
|
333
|
+
bot: probe.bot,
|
|
334
|
+
});
|
|
335
|
+
} catch (err) {
|
|
336
|
+
ctx.log?.warn?.(
|
|
337
|
+
`[${account.accountId}] NapCat probe threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const statusSink = createAccountStatusSink({
|
|
342
|
+
accountId: ctx.accountId,
|
|
343
|
+
setStatus: ctx.setStatus,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Determine WS port for reverse connection.
|
|
347
|
+
// Use gateway port + 100 + account index offset to avoid conflicts.
|
|
348
|
+
const wsPort = 18800 + listNapCatAccountIds(ctx.cfg).indexOf(account.accountId);
|
|
349
|
+
|
|
350
|
+
ctx.log?.info(`[${account.accountId}] starting NapCat provider${botLabel} mode=reverse-ws wsPort=${wsPort}`);
|
|
351
|
+
|
|
352
|
+
const { monitorNapCatProvider } = await import("./monitor.js");
|
|
353
|
+
return monitorNapCatProvider({
|
|
354
|
+
account,
|
|
355
|
+
config: ctx.cfg,
|
|
356
|
+
runtime: {
|
|
357
|
+
log: (msg) => ctx.log?.info(msg),
|
|
358
|
+
error: (msg) => ctx.log?.error?.(msg),
|
|
359
|
+
},
|
|
360
|
+
abortSignal: ctx.abortSignal,
|
|
361
|
+
wsPort,
|
|
362
|
+
statusSink,
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Direct JSON Schema — bypasses zod to avoid version/instance mismatch at runtime.
|
|
2
|
+
|
|
3
|
+
export const NapCatChannelConfigSchema = {
|
|
4
|
+
schema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
name: { type: "string" },
|
|
8
|
+
enabled: { type: "boolean" },
|
|
9
|
+
httpApi: { type: "string" },
|
|
10
|
+
accessToken: { type: "string" },
|
|
11
|
+
selfId: { type: "string" },
|
|
12
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist", "disabled"] },
|
|
13
|
+
allowFrom: {
|
|
14
|
+
type: "array",
|
|
15
|
+
items: { type: ["string", "number"] },
|
|
16
|
+
},
|
|
17
|
+
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
18
|
+
groupAllowFrom: {
|
|
19
|
+
type: "array",
|
|
20
|
+
items: { type: ["string", "number"] },
|
|
21
|
+
},
|
|
22
|
+
mediaMaxMb: { type: "number" },
|
|
23
|
+
responsePrefix: { type: "string" },
|
|
24
|
+
accounts: {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: true,
|
|
27
|
+
},
|
|
28
|
+
defaultAccount: { type: "string" },
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: true,
|
|
31
|
+
},
|
|
32
|
+
uiHints: {
|
|
33
|
+
httpApi: {
|
|
34
|
+
label: "NapCat HTTP API",
|
|
35
|
+
help: "NapCat OneBot 11 HTTP API 地址 (例: http://127.0.0.1:3000)",
|
|
36
|
+
placeholder: "http://127.0.0.1:3000",
|
|
37
|
+
},
|
|
38
|
+
accessToken: {
|
|
39
|
+
label: "Access Token",
|
|
40
|
+
sensitive: true,
|
|
41
|
+
help: "OneBot 11 API 鉴权 token (可选)",
|
|
42
|
+
},
|
|
43
|
+
selfId: {
|
|
44
|
+
label: "机器人 QQ 号",
|
|
45
|
+
help: "机器人的 QQ 号码,用于检测 @机器人",
|
|
46
|
+
},
|
|
47
|
+
dmPolicy: {
|
|
48
|
+
label: "私聊策略",
|
|
49
|
+
help: "allowlist=白名单, pairing=配对, open=开放, disabled=禁用",
|
|
50
|
+
},
|
|
51
|
+
allowFrom: {
|
|
52
|
+
label: "私聊白名单",
|
|
53
|
+
help: "允许私聊的 QQ 号列表",
|
|
54
|
+
},
|
|
55
|
+
groupPolicy: {
|
|
56
|
+
label: "群聊策略",
|
|
57
|
+
help: "allowlist=白名单, open=开放, disabled=禁用",
|
|
58
|
+
},
|
|
59
|
+
groupAllowFrom: {
|
|
60
|
+
label: "群聊白名单",
|
|
61
|
+
help: "允许在群聊中触发的 QQ 号列表",
|
|
62
|
+
},
|
|
63
|
+
mediaMaxMb: {
|
|
64
|
+
label: "最大媒体大小 (MB)",
|
|
65
|
+
advanced: true,
|
|
66
|
+
},
|
|
67
|
+
responsePrefix: {
|
|
68
|
+
label: "回复前缀",
|
|
69
|
+
advanced: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} as const;
|