@openclaw/qqbot 2026.5.12 → 2026.5.14-beta.2
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/dist/api.js +6 -6
- package/dist/channel-nG7HntSR.js +1102 -0
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-D_GS95Pk.js → channel.setup-D_8Ryfln.js} +1 -2
- package/dist/config-schema-BE5YP0NG.js +678 -0
- package/dist/{gateway-Bbsd8-ne.js → gateway-Bz_nu9BP.js} +98 -33
- package/dist/{handler-runtime-ZgvjnyoH.js → handler-runtime-DMMHU4NC.js} +3 -5
- package/dist/{outbound-BLWish89.js → outbound-DTQUASKu.js} +11 -3
- package/dist/{request-context-BwSBFANU.js → request-context-DiWgzdpn.js} +2 -2
- package/dist/{sender-BrpzCcXA.js → runtime-BFcYWYuk.js} +27 -6
- package/dist/runtime-api.js +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/package.json +5 -5
- package/dist/approval-cg0SVahb.js +0 -94
- package/dist/channel-CmK-n3Oz.js +0 -621
- package/dist/config-BT8KP_H1.js +0 -94
- package/dist/config-schema-BCxobX3l.js +0 -309
- package/dist/exec-approvals-W4oL9R6C.js +0 -138
- package/dist/narrowing-BoieBTIU.js +0 -25
- package/dist/resolve-D3lCbUze.js +0 -284
- package/dist/runtime-CZyFkpnB.js +0 -18
- package/dist/target-parser-C8HaD-ik.js +0 -245
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as qqbotPlugin } from "./channel-
|
|
1
|
+
import { t as qqbotPlugin } from "./channel-nG7HntSR.js";
|
|
2
2
|
export { qqbotPlugin };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-BCxobX3l.js";
|
|
1
|
+
import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-BE5YP0NG.js";
|
|
3
2
|
//#region extensions/qqbot/src/channel.setup.ts
|
|
4
3
|
/**
|
|
5
4
|
* Setup-only QQBot plugin — lightweight subset used during `openclaw onboard`
|
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { a as normalizeStringifiedOptionalString, c as getPlatformAdapter, d as registerPlatformAdapterFactory, l as hasPlatformAdapter, n as normalizeLowercaseStringOrEmpty, o as readStringField, r as normalizeOptionalLowercaseString, t as asOptionalObjectRecord, u as registerPlatformAdapter } from "./string-normalize-C46CS-F1.js";
|
|
2
|
+
import { buildSecretInputSchema, coerceSecretRef, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
3
|
+
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
|
|
6
|
+
import { applyAccountNameToChannelSection, deleteAccountFromConfigSection, setAccountEnabledInConfigSection } from "openclaw/plugin-sdk/core";
|
|
7
|
+
import { DEFAULT_ACCOUNT_ID, createStandardChannelSetupStatus, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
8
|
+
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
|
9
|
+
import { AllowFromListSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
//#region extensions/qqbot/src/bridge/logger.ts
|
|
12
|
+
let _logger = null;
|
|
13
|
+
/** Register the framework logger. Called once in startGateway(). */
|
|
14
|
+
function setBridgeLogger(logger) {
|
|
15
|
+
_logger = logger;
|
|
16
|
+
}
|
|
17
|
+
/** Get the bridge logger. Falls back to console if not yet registered. */
|
|
18
|
+
function getBridgeLogger() {
|
|
19
|
+
return _logger ?? {
|
|
20
|
+
info: (msg) => console.log(msg),
|
|
21
|
+
error: (msg) => console.error(msg),
|
|
22
|
+
debug: (msg) => console.log(msg)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region extensions/qqbot/src/bridge/bootstrap.ts
|
|
27
|
+
/**
|
|
28
|
+
* Bootstrap the PlatformAdapter for the built-in version.
|
|
29
|
+
*
|
|
30
|
+
* ## Design
|
|
31
|
+
*
|
|
32
|
+
* The adapter is registered via two complementary mechanisms:
|
|
33
|
+
*
|
|
34
|
+
* 1. **Factory registration** (`registerPlatformAdapterFactory`) — a lightweight
|
|
35
|
+
* callback stored in `adapter/index.ts` that is invoked lazily by
|
|
36
|
+
* `getPlatformAdapter()` on first access. This guarantees the adapter is
|
|
37
|
+
* available regardless of module evaluation order or bundler chunk splitting.
|
|
38
|
+
*
|
|
39
|
+
* 2. **Eager side-effect** (`ensurePlatformAdapter()`) — called at module
|
|
40
|
+
* evaluation time when `channel.ts` imports this file. Provides the adapter
|
|
41
|
+
* immediately for code that runs synchronously during startup.
|
|
42
|
+
*
|
|
43
|
+
* Heavy async-only dependencies (`media-runtime`, `config-runtime`,
|
|
44
|
+
* `approval-gateway-runtime`) are lazy-imported inside each async method body
|
|
45
|
+
* so that this module evaluates with minimal overhead.
|
|
46
|
+
*
|
|
47
|
+
* Synchronous dependencies (`secret-input`, `temp-path`) are imported
|
|
48
|
+
* statically at the top level so they work reliably in both production and
|
|
49
|
+
* vitest (which resolves bare specifiers via `resolve.alias`, not Node CJS).
|
|
50
|
+
*/
|
|
51
|
+
function createBuiltinAdapter() {
|
|
52
|
+
return {
|
|
53
|
+
async validateRemoteUrl(_url, _options) {},
|
|
54
|
+
async resolveSecret(value) {
|
|
55
|
+
if (typeof value === "string") return value || void 0;
|
|
56
|
+
},
|
|
57
|
+
async downloadFile(url, destDir, filename) {
|
|
58
|
+
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
|
|
59
|
+
const result = await readRemoteMediaBuffer({
|
|
60
|
+
url,
|
|
61
|
+
filePathHint: filename
|
|
62
|
+
});
|
|
63
|
+
const fs = await import("node:fs");
|
|
64
|
+
const path = await import("node:path");
|
|
65
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
66
|
+
const destPath = path.join(destDir, filename ?? "download");
|
|
67
|
+
fs.writeFileSync(destPath, result.buffer);
|
|
68
|
+
return destPath;
|
|
69
|
+
},
|
|
70
|
+
async fetchMedia(options) {
|
|
71
|
+
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
|
|
72
|
+
const result = await readRemoteMediaBuffer({
|
|
73
|
+
url: options.url,
|
|
74
|
+
filePathHint: options.filePathHint,
|
|
75
|
+
maxBytes: options.maxBytes,
|
|
76
|
+
maxRedirects: options.maxRedirects,
|
|
77
|
+
ssrfPolicy: options.ssrfPolicy,
|
|
78
|
+
requestInit: options.requestInit
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
buffer: result.buffer,
|
|
82
|
+
fileName: result.fileName
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
getTempDir() {
|
|
86
|
+
return resolvePreferredOpenClawTmpDir();
|
|
87
|
+
},
|
|
88
|
+
hasConfiguredSecret(value) {
|
|
89
|
+
return hasConfiguredSecretInput(value);
|
|
90
|
+
},
|
|
91
|
+
normalizeSecretInputString(value) {
|
|
92
|
+
return normalizeSecretInputString(value) ?? void 0;
|
|
93
|
+
},
|
|
94
|
+
resolveSecretInputString(params) {
|
|
95
|
+
return normalizeResolvedSecretInputString(params) ?? void 0;
|
|
96
|
+
},
|
|
97
|
+
async resolveApproval(approvalId, decision) {
|
|
98
|
+
try {
|
|
99
|
+
const { getRuntimeConfig } = await import("openclaw/plugin-sdk/runtime-config-snapshot");
|
|
100
|
+
const { resolveApprovalOverGateway } = await import("openclaw/plugin-sdk/approval-gateway-runtime");
|
|
101
|
+
await resolveApprovalOverGateway({
|
|
102
|
+
cfg: getRuntimeConfig(),
|
|
103
|
+
approvalId,
|
|
104
|
+
decision,
|
|
105
|
+
clientDisplayName: "QQBot Approval Handler"
|
|
106
|
+
});
|
|
107
|
+
return true;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
getBridgeLogger().error(`[qqbot] resolveApproval failed: ${String(err)}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Ensure the built-in PlatformAdapter is registered.
|
|
117
|
+
*
|
|
118
|
+
* Safe to call multiple times — only registers on the first invocation.
|
|
119
|
+
* Exported for backward compatibility with code that calls it explicitly.
|
|
120
|
+
*/
|
|
121
|
+
function ensurePlatformAdapter() {
|
|
122
|
+
if (!hasPlatformAdapter()) registerPlatformAdapter(createBuiltinAdapter());
|
|
123
|
+
}
|
|
124
|
+
registerPlatformAdapterFactory(createBuiltinAdapter);
|
|
125
|
+
ensurePlatformAdapter();
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region extensions/qqbot/src/engine/config/resolve.ts
|
|
128
|
+
/**
|
|
129
|
+
* QQBot config resolution (pure logic layer).
|
|
130
|
+
* QQBot 配置解析(纯逻辑层)。
|
|
131
|
+
*
|
|
132
|
+
* Resolves account IDs, default account selection, and base account
|
|
133
|
+
* info from raw config objects. Secret/credential resolution is
|
|
134
|
+
* intentionally left to the outer layer (src/bridge/config.ts) so that
|
|
135
|
+
* this module stays framework-agnostic and self-contained.
|
|
136
|
+
*/
|
|
137
|
+
/**
|
|
138
|
+
* Default account ID, used for the unnamed top-level account.
|
|
139
|
+
* 默认账号 ID,用于顶层配置中未命名的账号。
|
|
140
|
+
*/
|
|
141
|
+
const DEFAULT_ACCOUNT_ID$2 = "default";
|
|
142
|
+
function normalizeAppId(raw) {
|
|
143
|
+
if (typeof raw === "string") return raw.trim();
|
|
144
|
+
if (typeof raw === "number") return String(raw);
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
function normalizeAccountConfig(account) {
|
|
148
|
+
if (!account) return {};
|
|
149
|
+
const audioPolicy = asOptionalObjectRecord(account.audioFormatPolicy);
|
|
150
|
+
return {
|
|
151
|
+
...account,
|
|
152
|
+
...audioPolicy ? { audioFormatPolicy: { ...audioPolicy } } : {}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function readQQBotSection(cfg) {
|
|
156
|
+
return asOptionalObjectRecord(asOptionalObjectRecord(cfg.channels)?.qqbot);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* List all configured QQBot account IDs.
|
|
160
|
+
* 列出所有已配置的 QQBot 账号 ID。
|
|
161
|
+
*/
|
|
162
|
+
function listAccountIds(cfg) {
|
|
163
|
+
const ids = /* @__PURE__ */ new Set();
|
|
164
|
+
const qqbot = readQQBotSection(cfg);
|
|
165
|
+
if (qqbot?.appId || process.env.QQBOT_APP_ID) ids.add(DEFAULT_ACCOUNT_ID$2);
|
|
166
|
+
if (qqbot?.accounts) {
|
|
167
|
+
for (const accountId of Object.keys(qqbot.accounts)) if (qqbot.accounts[accountId]?.appId) ids.add(accountId);
|
|
168
|
+
}
|
|
169
|
+
return Array.from(ids);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Resolve the default QQBot account ID.
|
|
173
|
+
* 解析默认 QQBot 账号 ID(优先级:defaultAccount > 顶层 appId > 第一个命名账号)。
|
|
174
|
+
*/
|
|
175
|
+
function resolveDefaultAccountId(cfg) {
|
|
176
|
+
const qqbot = readQQBotSection(cfg);
|
|
177
|
+
const configuredDefaultAccountId = normalizeOptionalLowercaseString(qqbot?.defaultAccount);
|
|
178
|
+
if (configuredDefaultAccountId && (configuredDefaultAccountId === "default" || Boolean(qqbot?.accounts?.[configuredDefaultAccountId]?.appId))) return configuredDefaultAccountId;
|
|
179
|
+
if (qqbot?.appId || process.env.QQBOT_APP_ID) return DEFAULT_ACCOUNT_ID$2;
|
|
180
|
+
if (qqbot?.accounts) {
|
|
181
|
+
const ids = Object.keys(qqbot.accounts);
|
|
182
|
+
if (ids.length > 0) return ids[0];
|
|
183
|
+
}
|
|
184
|
+
return DEFAULT_ACCOUNT_ID$2;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Resolve base account info (without credentials).
|
|
188
|
+
* 解析账号基础信息(不含凭证)。
|
|
189
|
+
*
|
|
190
|
+
* Resolves everything except Secret/credential fields. The outer
|
|
191
|
+
* config.ts layer calls this and adds Secret handling on top.
|
|
192
|
+
*/
|
|
193
|
+
function resolveAccountBase(cfg, accountId) {
|
|
194
|
+
const resolvedAccountId = accountId ?? resolveDefaultAccountId(cfg);
|
|
195
|
+
const qqbot = readQQBotSection(cfg);
|
|
196
|
+
let accountConfig = {};
|
|
197
|
+
let appId = "";
|
|
198
|
+
if (resolvedAccountId === "default") {
|
|
199
|
+
accountConfig = normalizeAccountConfig(asOptionalObjectRecord(qqbot));
|
|
200
|
+
appId = normalizeAppId(qqbot?.appId);
|
|
201
|
+
} else {
|
|
202
|
+
const account = qqbot?.accounts?.[resolvedAccountId];
|
|
203
|
+
accountConfig = normalizeAccountConfig(asOptionalObjectRecord(account));
|
|
204
|
+
appId = normalizeAppId(asOptionalObjectRecord(account)?.appId);
|
|
205
|
+
}
|
|
206
|
+
if (!appId && process.env.QQBOT_APP_ID && resolvedAccountId === "default") appId = normalizeAppId(process.env.QQBOT_APP_ID);
|
|
207
|
+
return {
|
|
208
|
+
accountId: resolvedAccountId,
|
|
209
|
+
name: readStringField(accountConfig, "name"),
|
|
210
|
+
enabled: accountConfig.enabled !== false,
|
|
211
|
+
appId,
|
|
212
|
+
systemPrompt: readStringField(accountConfig, "systemPrompt"),
|
|
213
|
+
markdownSupport: accountConfig.markdownSupport !== false,
|
|
214
|
+
config: accountConfig
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Apply account config updates into a raw config object. */
|
|
218
|
+
function applyAccountConfig(cfg, accountId, input) {
|
|
219
|
+
const next = { ...cfg };
|
|
220
|
+
const channels = asOptionalObjectRecord(cfg.channels) ?? {};
|
|
221
|
+
const existingQQBot = asOptionalObjectRecord(channels.qqbot) ?? {};
|
|
222
|
+
if (accountId === "default") {
|
|
223
|
+
const allowFrom = existingQQBot.allowFrom ?? ["*"];
|
|
224
|
+
next.channels = {
|
|
225
|
+
...channels,
|
|
226
|
+
qqbot: {
|
|
227
|
+
...existingQQBot,
|
|
228
|
+
enabled: true,
|
|
229
|
+
allowFrom,
|
|
230
|
+
...input.appId ? { appId: input.appId } : {},
|
|
231
|
+
...input.clientSecret ? {
|
|
232
|
+
clientSecret: input.clientSecret,
|
|
233
|
+
clientSecretFile: void 0
|
|
234
|
+
} : input.clientSecretFile ? {
|
|
235
|
+
clientSecretFile: input.clientSecretFile,
|
|
236
|
+
clientSecret: void 0
|
|
237
|
+
} : {},
|
|
238
|
+
...input.name ? { name: input.name } : {}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
const accounts = existingQQBot.accounts ?? {};
|
|
243
|
+
const existingAccount = accounts[accountId] ?? {};
|
|
244
|
+
const allowFrom = existingAccount.allowFrom ?? ["*"];
|
|
245
|
+
next.channels = {
|
|
246
|
+
...channels,
|
|
247
|
+
qqbot: {
|
|
248
|
+
...existingQQBot,
|
|
249
|
+
enabled: true,
|
|
250
|
+
accounts: {
|
|
251
|
+
...accounts,
|
|
252
|
+
[accountId]: {
|
|
253
|
+
...existingAccount,
|
|
254
|
+
enabled: true,
|
|
255
|
+
allowFrom,
|
|
256
|
+
...input.appId ? { appId: input.appId } : {},
|
|
257
|
+
...input.clientSecret ? {
|
|
258
|
+
clientSecret: input.clientSecret,
|
|
259
|
+
clientSecretFile: void 0
|
|
260
|
+
} : input.clientSecretFile ? {
|
|
261
|
+
clientSecretFile: input.clientSecretFile,
|
|
262
|
+
clientSecret: void 0
|
|
263
|
+
} : {},
|
|
264
|
+
...input.name ? { name: input.name } : {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return next;
|
|
271
|
+
}
|
|
272
|
+
/** Check whether a QQBot account has been fully configured. */
|
|
273
|
+
function isAccountConfigured(account) {
|
|
274
|
+
return Boolean(account?.appId && (Boolean(account?.clientSecret) || getPlatformAdapter().hasConfiguredSecret(account?.config?.clientSecret) || Boolean(account?.config?.clientSecretFile?.trim())));
|
|
275
|
+
}
|
|
276
|
+
/** Build a summary description of an account. */
|
|
277
|
+
function describeAccount(account) {
|
|
278
|
+
return {
|
|
279
|
+
accountId: account?.accountId ?? "default",
|
|
280
|
+
name: account?.name,
|
|
281
|
+
enabled: account?.enabled ?? false,
|
|
282
|
+
configured: isAccountConfigured(account),
|
|
283
|
+
tokenSource: account?.secretSource
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** Normalize allowFrom entries into uppercase strings without the qqbot: prefix. */
|
|
287
|
+
function formatAllowFrom(allowFrom) {
|
|
288
|
+
return (allowFrom ?? []).map((entry) => normalizeStringifiedOptionalString(entry)).filter((entry) => Boolean(entry)).map((entry) => entry.replace(/^qqbot:/i, "")).map((entry) => entry.toUpperCase());
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region extensions/qqbot/src/bridge/config.ts
|
|
292
|
+
const DEFAULT_ACCOUNT_ID$1 = DEFAULT_ACCOUNT_ID$2;
|
|
293
|
+
function assertNotLegacySecretRefMarker(value, path) {
|
|
294
|
+
const normalized = normalizeSecretInputString(value);
|
|
295
|
+
if (!normalized || !/^secretref(?:-env)?:/i.test(normalized)) return;
|
|
296
|
+
throw new Error(`${path}: legacy SecretRef marker strings are not valid QQ Bot clientSecret values; use a structured SecretRef object instead.`);
|
|
297
|
+
}
|
|
298
|
+
function resolveEnvSecretRefValue(params) {
|
|
299
|
+
const ref = coerceSecretRef(params.value, params.cfg.secrets?.defaults);
|
|
300
|
+
if (!ref || ref.source !== "env") return;
|
|
301
|
+
const providerConfig = params.cfg.secrets?.providers?.[ref.provider];
|
|
302
|
+
if (providerConfig) {
|
|
303
|
+
if (providerConfig.source !== "env") throw new Error(`Secret provider "${ref.provider}" has source "${providerConfig.source}" but ref requests "env".`);
|
|
304
|
+
if (providerConfig.allowlist && !providerConfig.allowlist.includes(ref.id)) throw new Error(`Environment variable "${ref.id}" is not allowlisted in secrets.providers.${ref.provider}.allowlist.`);
|
|
305
|
+
} else if (ref.provider !== resolveDefaultSecretProviderAlias(params.cfg, "env")) throw new Error(`Secret provider "${ref.provider}" is not configured (ref: env:${ref.provider}:${ref.id}).`);
|
|
306
|
+
return normalizeSecretInputString((params.env ?? process.env)[ref.id]);
|
|
307
|
+
}
|
|
308
|
+
function resolveQQBotClientSecretInput(params) {
|
|
309
|
+
assertNotLegacySecretRefMarker(params.value, params.path);
|
|
310
|
+
const envSecret = resolveEnvSecretRefValue({
|
|
311
|
+
cfg: params.cfg,
|
|
312
|
+
value: params.value
|
|
313
|
+
});
|
|
314
|
+
if (envSecret) return envSecret;
|
|
315
|
+
return getPlatformAdapter().resolveSecretInputString({
|
|
316
|
+
value: params.value,
|
|
317
|
+
path: params.path
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/** List all configured QQBot account IDs. */
|
|
321
|
+
function listQQBotAccountIds(cfg) {
|
|
322
|
+
return listAccountIds(cfg);
|
|
323
|
+
}
|
|
324
|
+
/** Resolve the default QQBot account ID. */
|
|
325
|
+
function resolveDefaultQQBotAccountId(cfg) {
|
|
326
|
+
return resolveDefaultAccountId(cfg);
|
|
327
|
+
}
|
|
328
|
+
/** Resolve QQBot account config for runtime or setup flows. */
|
|
329
|
+
function resolveQQBotAccount(cfg, accountId, opts) {
|
|
330
|
+
const base = resolveAccountBase(cfg, accountId);
|
|
331
|
+
const qqbot = cfg.channels?.qqbot;
|
|
332
|
+
/**
|
|
333
|
+
* Legacy top-level account uses `channels.qqbot` as the base, but per-account
|
|
334
|
+
* fields (allowFrom, streaming, …) often live under `accounts.default`.
|
|
335
|
+
* Merge that slice so runtime sees `config.streaming` etc.
|
|
336
|
+
*/
|
|
337
|
+
const accountConfig = base.accountId === DEFAULT_ACCOUNT_ID$1 ? {
|
|
338
|
+
...qqbot,
|
|
339
|
+
...qqbot?.accounts?.[DEFAULT_ACCOUNT_ID$1]
|
|
340
|
+
} : qqbot?.accounts?.[base.accountId] ?? {};
|
|
341
|
+
let clientSecret = "";
|
|
342
|
+
let secretSource = "none";
|
|
343
|
+
const clientSecretPath = base.accountId === DEFAULT_ACCOUNT_ID$1 ? "channels.qqbot.clientSecret" : `channels.qqbot.accounts.${base.accountId}.clientSecret`;
|
|
344
|
+
const adapter = getPlatformAdapter();
|
|
345
|
+
if (adapter.hasConfiguredSecret(accountConfig.clientSecret)) {
|
|
346
|
+
clientSecret = opts?.allowUnresolvedSecretRef ? adapter.normalizeSecretInputString(accountConfig.clientSecret) ?? "" : resolveQQBotClientSecretInput({
|
|
347
|
+
cfg,
|
|
348
|
+
value: accountConfig.clientSecret,
|
|
349
|
+
path: clientSecretPath
|
|
350
|
+
}) ?? "";
|
|
351
|
+
secretSource = "config";
|
|
352
|
+
} else if (accountConfig.clientSecretFile) try {
|
|
353
|
+
clientSecret = fs.readFileSync(accountConfig.clientSecretFile, "utf8").trim();
|
|
354
|
+
secretSource = "file";
|
|
355
|
+
} catch {
|
|
356
|
+
secretSource = "none";
|
|
357
|
+
}
|
|
358
|
+
else if (process.env.QQBOT_CLIENT_SECRET && base.accountId === DEFAULT_ACCOUNT_ID$1) {
|
|
359
|
+
clientSecret = process.env.QQBOT_CLIENT_SECRET;
|
|
360
|
+
secretSource = "env";
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
accountId: base.accountId,
|
|
364
|
+
name: accountConfig.name,
|
|
365
|
+
enabled: base.enabled,
|
|
366
|
+
appId: base.appId,
|
|
367
|
+
clientSecret,
|
|
368
|
+
secretSource,
|
|
369
|
+
systemPrompt: base.systemPrompt,
|
|
370
|
+
markdownSupport: base.markdownSupport,
|
|
371
|
+
config: accountConfig
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** Apply account config updates back into the OpenClaw config object. */
|
|
375
|
+
function applyQQBotAccountConfig(cfg, accountId, input) {
|
|
376
|
+
return applyAccountConfig(cfg, accountId, input);
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region extensions/qqbot/src/engine/config/setup-logic.ts
|
|
380
|
+
/**
|
|
381
|
+
* QQBot setup business logic (pure layer).
|
|
382
|
+
* QQBot setup 相关纯业务逻辑。
|
|
383
|
+
*
|
|
384
|
+
* Token parsing, input validation, and setup config application.
|
|
385
|
+
* All functions are framework-agnostic and operate on plain objects.
|
|
386
|
+
*/
|
|
387
|
+
/** Parse an inline "appId:clientSecret" token string. */
|
|
388
|
+
function parseInlineToken(token) {
|
|
389
|
+
const colonIdx = token.indexOf(":");
|
|
390
|
+
if (colonIdx <= 0 || colonIdx === token.length - 1) return null;
|
|
391
|
+
const appId = token.slice(0, colonIdx).trim();
|
|
392
|
+
const clientSecret = token.slice(colonIdx + 1).trim();
|
|
393
|
+
if (!appId || !clientSecret) return null;
|
|
394
|
+
return {
|
|
395
|
+
appId,
|
|
396
|
+
clientSecret
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/** Validate setup input for a QQBot account. Returns an error string or null. */
|
|
400
|
+
function validateSetupInput(accountId, input) {
|
|
401
|
+
if (!input.token && !input.tokenFile && !input.useEnv) return "QQBot requires --token (format: appId:clientSecret) or --use-env";
|
|
402
|
+
if (input.useEnv && accountId !== "default") return "QQBot --use-env only supports the default account";
|
|
403
|
+
if (input.token && !parseInlineToken(input.token)) return "QQBot --token must be in appId:clientSecret format";
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
/** Apply setup input to account config. Returns updated config. */
|
|
407
|
+
function applySetupAccountConfig(cfg, accountId, input) {
|
|
408
|
+
if (input.useEnv && accountId !== "default") return cfg;
|
|
409
|
+
let appId = "";
|
|
410
|
+
let clientSecret = "";
|
|
411
|
+
if (input.token) {
|
|
412
|
+
const parsed = parseInlineToken(input.token);
|
|
413
|
+
if (!parsed) return cfg;
|
|
414
|
+
appId = parsed.appId;
|
|
415
|
+
clientSecret = parsed.clientSecret;
|
|
416
|
+
}
|
|
417
|
+
if (!appId && !input.tokenFile && !input.useEnv) return cfg;
|
|
418
|
+
return applyAccountConfig(cfg, accountId, {
|
|
419
|
+
appId,
|
|
420
|
+
clientSecret,
|
|
421
|
+
clientSecretFile: input.tokenFile,
|
|
422
|
+
name: input.name
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region extensions/qqbot/src/bridge/config-shared.ts
|
|
427
|
+
const qqbotMeta = {
|
|
428
|
+
id: "qqbot",
|
|
429
|
+
label: "QQ Bot",
|
|
430
|
+
selectionLabel: "QQ Bot (Bot API)",
|
|
431
|
+
docsPath: "/channels/qqbot",
|
|
432
|
+
blurb: "Connect to QQ via official QQ Bot API",
|
|
433
|
+
order: 50
|
|
434
|
+
};
|
|
435
|
+
function validateQQBotSetupInput(params) {
|
|
436
|
+
return validateSetupInput(params.accountId, params.input);
|
|
437
|
+
}
|
|
438
|
+
function applyQQBotSetupAccountConfig(params) {
|
|
439
|
+
return applySetupAccountConfig(params.cfg, params.accountId, params.input);
|
|
440
|
+
}
|
|
441
|
+
function isQQBotConfigured(account) {
|
|
442
|
+
return isAccountConfigured(account);
|
|
443
|
+
}
|
|
444
|
+
function describeQQBotAccount(account) {
|
|
445
|
+
return describeAccount(account);
|
|
446
|
+
}
|
|
447
|
+
function formatQQBotAllowFrom(params) {
|
|
448
|
+
return formatAllowFrom(params.allowFrom);
|
|
449
|
+
}
|
|
450
|
+
const qqbotConfigAdapter = {
|
|
451
|
+
listAccountIds: (cfg) => listQQBotAccountIds(cfg),
|
|
452
|
+
resolveAccount: (cfg, accountId) => resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true }),
|
|
453
|
+
defaultAccountId: (cfg) => resolveDefaultQQBotAccountId(cfg),
|
|
454
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
|
|
455
|
+
cfg,
|
|
456
|
+
sectionKey: "qqbot",
|
|
457
|
+
accountId,
|
|
458
|
+
enabled,
|
|
459
|
+
allowTopLevel: true
|
|
460
|
+
}),
|
|
461
|
+
deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
|
|
462
|
+
cfg,
|
|
463
|
+
sectionKey: "qqbot",
|
|
464
|
+
accountId,
|
|
465
|
+
clearBaseFields: [
|
|
466
|
+
"appId",
|
|
467
|
+
"clientSecret",
|
|
468
|
+
"clientSecretFile",
|
|
469
|
+
"name"
|
|
470
|
+
]
|
|
471
|
+
}),
|
|
472
|
+
isConfigured: isQQBotConfigured,
|
|
473
|
+
describeAccount: describeQQBotAccount,
|
|
474
|
+
resolveAllowFrom: ({ cfg, accountId }) => resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true }).config?.allowFrom,
|
|
475
|
+
formatAllowFrom: ({ allowFrom }) => formatQQBotAllowFrom({ allowFrom })
|
|
476
|
+
};
|
|
477
|
+
const qqbotSetupAdapterShared = {
|
|
478
|
+
resolveAccountId: ({ cfg, accountId }) => normalizeLowercaseStringOrEmpty(accountId) || resolveDefaultQQBotAccountId(cfg),
|
|
479
|
+
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
480
|
+
cfg,
|
|
481
|
+
channelKey: "qqbot",
|
|
482
|
+
accountId,
|
|
483
|
+
name
|
|
484
|
+
}),
|
|
485
|
+
validateInput: ({ accountId, input }) => validateQQBotSetupInput({
|
|
486
|
+
accountId,
|
|
487
|
+
input
|
|
488
|
+
}),
|
|
489
|
+
applyAccountConfig: ({ cfg, accountId, input }) => applyQQBotSetupAccountConfig({
|
|
490
|
+
cfg,
|
|
491
|
+
accountId,
|
|
492
|
+
input
|
|
493
|
+
})
|
|
494
|
+
};
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region extensions/qqbot/src/bridge/setup/finalize.ts
|
|
497
|
+
function isQQBotAccountConfigured(cfg, accountId) {
|
|
498
|
+
const account = resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true });
|
|
499
|
+
return Boolean(account.appId && account.clientSecret);
|
|
500
|
+
}
|
|
501
|
+
async function linkViaQrCode(params) {
|
|
502
|
+
try {
|
|
503
|
+
const { qrConnect } = await import("@tencent-connect/qqbot-connector");
|
|
504
|
+
const accounts = await qrConnect({ source: "openclaw" });
|
|
505
|
+
if (accounts.length === 0) {
|
|
506
|
+
await params.prompter.note("未获取到任何 QQ Bot 账号信息。", "QQ Bot");
|
|
507
|
+
return params.cfg;
|
|
508
|
+
}
|
|
509
|
+
let next = params.cfg;
|
|
510
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
511
|
+
const { appId, appSecret } = accounts[i];
|
|
512
|
+
const targetAccountId = i === 0 ? params.accountId : appId;
|
|
513
|
+
next = applyQQBotAccountConfig(next, targetAccountId, {
|
|
514
|
+
appId,
|
|
515
|
+
clientSecret: appSecret
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (accounts.length === 1) params.runtime.log(`✔ QQ Bot 绑定成功!(AppID: ${accounts[0].appId})`);
|
|
519
|
+
else {
|
|
520
|
+
const idList = accounts.map((a) => a.appId).join(", ");
|
|
521
|
+
params.runtime.log(`✔ ${accounts.length} 个 QQ Bot 绑定成功!(AppID: ${idList})`);
|
|
522
|
+
}
|
|
523
|
+
return next;
|
|
524
|
+
} catch (error) {
|
|
525
|
+
params.runtime.error(`QQ Bot 绑定失败: ${String(error)}`);
|
|
526
|
+
await params.prompter.note(["绑定失败,您可以稍后手动配置。", `文档: ${formatDocsLink("/channels/qqbot", "qqbot")}`].join("\n"), "QQ Bot");
|
|
527
|
+
return params.cfg;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function linkViaManualInput(params) {
|
|
531
|
+
const appId = await params.prompter.text({
|
|
532
|
+
message: "请输入 QQ Bot AppID",
|
|
533
|
+
validate: (value) => value.trim() ? void 0 : "AppID 不能为空"
|
|
534
|
+
});
|
|
535
|
+
const appSecret = await params.prompter.text({
|
|
536
|
+
message: "请输入 QQ Bot AppSecret",
|
|
537
|
+
validate: (value) => value.trim() ? void 0 : "AppSecret 不能为空"
|
|
538
|
+
});
|
|
539
|
+
const next = applyQQBotAccountConfig(params.cfg, params.accountId, {
|
|
540
|
+
appId: appId.trim(),
|
|
541
|
+
clientSecret: appSecret.trim()
|
|
542
|
+
});
|
|
543
|
+
await params.prompter.note("✔ QQ Bot 配置完成!", "QQ Bot");
|
|
544
|
+
return next;
|
|
545
|
+
}
|
|
546
|
+
async function finalizeQQBotSetup(params) {
|
|
547
|
+
const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
|
|
548
|
+
let next = params.cfg;
|
|
549
|
+
const configured = isQQBotAccountConfigured(next, accountId);
|
|
550
|
+
const mode = await params.prompter.select({
|
|
551
|
+
message: configured ? "QQ 已绑定,选择操作" : "选择 QQ 绑定方式",
|
|
552
|
+
options: [
|
|
553
|
+
{
|
|
554
|
+
value: "qr",
|
|
555
|
+
label: "扫码绑定(推荐)",
|
|
556
|
+
hint: "使用 QQ 扫描二维码自动完成绑定"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
value: "manual",
|
|
560
|
+
label: "手动输入 QQ Bot AppID 和 AppSecret",
|
|
561
|
+
hint: "需到 QQ 开放平台 q.qq.com 查看"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
value: "skip",
|
|
565
|
+
label: configured ? "保持当前配置" : "稍后配置"
|
|
566
|
+
}
|
|
567
|
+
]
|
|
568
|
+
});
|
|
569
|
+
if (mode === "qr") next = await linkViaQrCode({
|
|
570
|
+
cfg: next,
|
|
571
|
+
accountId,
|
|
572
|
+
prompter: params.prompter,
|
|
573
|
+
runtime: params.runtime
|
|
574
|
+
});
|
|
575
|
+
else if (mode === "manual") next = await linkViaManualInput({
|
|
576
|
+
cfg: next,
|
|
577
|
+
accountId,
|
|
578
|
+
prompter: params.prompter
|
|
579
|
+
});
|
|
580
|
+
else if (!configured) await params.prompter.note(["您可以稍后运行以下命令重新选择 QQ Bot 进行配置:", " openclaw channels add"].join("\n"), "QQ Bot");
|
|
581
|
+
return { cfg: next };
|
|
582
|
+
}
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region extensions/qqbot/src/bridge/setup/surface.ts
|
|
585
|
+
const channel = "qqbot";
|
|
586
|
+
const qqbotSetupWizard = {
|
|
587
|
+
channel,
|
|
588
|
+
status: createStandardChannelSetupStatus({
|
|
589
|
+
channelLabel: "QQ Bot",
|
|
590
|
+
configuredLabel: "configured",
|
|
591
|
+
unconfiguredLabel: "needs AppID + AppSercet",
|
|
592
|
+
configuredHint: "configured",
|
|
593
|
+
unconfiguredHint: "needs AppID + AppSercet",
|
|
594
|
+
configuredScore: 1,
|
|
595
|
+
unconfiguredScore: 6,
|
|
596
|
+
resolveConfigured: ({ cfg, accountId }) => (accountId ? [accountId] : listQQBotAccountIds(cfg)).some((resolvedAccountId) => {
|
|
597
|
+
return isAccountConfigured(resolveQQBotAccount(cfg, resolvedAccountId, { allowUnresolvedSecretRef: true }));
|
|
598
|
+
})
|
|
599
|
+
}),
|
|
600
|
+
credentials: [],
|
|
601
|
+
finalize: async ({ cfg, accountId, forceAllowFrom, prompter, runtime }) => await finalizeQQBotSetup({
|
|
602
|
+
cfg,
|
|
603
|
+
accountId,
|
|
604
|
+
forceAllowFrom,
|
|
605
|
+
prompter,
|
|
606
|
+
runtime
|
|
607
|
+
}),
|
|
608
|
+
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
|
|
609
|
+
};
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region extensions/qqbot/src/config-schema.ts
|
|
612
|
+
const AudioFormatPolicySchema = z.object({
|
|
613
|
+
sttDirectFormats: z.array(z.string()).optional(),
|
|
614
|
+
uploadDirectFormats: z.array(z.string()).optional(),
|
|
615
|
+
transcodeEnabled: z.boolean().optional()
|
|
616
|
+
}).optional();
|
|
617
|
+
const QQBotSttSchema = z.object({
|
|
618
|
+
enabled: z.boolean().optional(),
|
|
619
|
+
provider: z.string().optional(),
|
|
620
|
+
baseUrl: z.string().optional(),
|
|
621
|
+
apiKey: z.string().optional(),
|
|
622
|
+
model: z.string().optional()
|
|
623
|
+
}).strict().optional();
|
|
624
|
+
/** When `true`, same as `mode: "partial"` and `c2cStreamApi: true` for C2C. Object form kept for legacy configs. */
|
|
625
|
+
const QQBotStreamingSchema = z.union([z.boolean(), z.object({
|
|
626
|
+
/** "partial" (default) enables block streaming; "off" disables it. */
|
|
627
|
+
mode: z.enum(["off", "partial"]).default("partial"),
|
|
628
|
+
/** @deprecated Prefer `streaming: true`. */
|
|
629
|
+
c2cStreamApi: z.boolean().optional()
|
|
630
|
+
}).passthrough()]).optional();
|
|
631
|
+
const QQBotExecApprovalsSchema = z.object({
|
|
632
|
+
enabled: z.union([z.boolean(), z.literal("auto")]).optional(),
|
|
633
|
+
approvers: z.array(z.string()).optional(),
|
|
634
|
+
agentFilter: z.array(z.string()).optional(),
|
|
635
|
+
sessionFilter: z.array(z.string()).optional(),
|
|
636
|
+
target: z.enum([
|
|
637
|
+
"dm",
|
|
638
|
+
"channel",
|
|
639
|
+
"both"
|
|
640
|
+
]).optional()
|
|
641
|
+
}).strict().optional();
|
|
642
|
+
const QQBotDmPolicySchema = z.enum([
|
|
643
|
+
"open",
|
|
644
|
+
"allowlist",
|
|
645
|
+
"disabled"
|
|
646
|
+
]).optional();
|
|
647
|
+
const QQBotGroupPolicySchema = z.enum([
|
|
648
|
+
"open",
|
|
649
|
+
"allowlist",
|
|
650
|
+
"disabled"
|
|
651
|
+
]).optional();
|
|
652
|
+
const QQBotAccountSchema = z.object({
|
|
653
|
+
enabled: z.boolean().optional(),
|
|
654
|
+
name: z.string().optional(),
|
|
655
|
+
appId: z.string().optional(),
|
|
656
|
+
clientSecret: buildSecretInputSchema().optional(),
|
|
657
|
+
clientSecretFile: z.string().optional(),
|
|
658
|
+
allowFrom: AllowFromListSchema,
|
|
659
|
+
groupAllowFrom: AllowFromListSchema,
|
|
660
|
+
dmPolicy: QQBotDmPolicySchema,
|
|
661
|
+
groupPolicy: QQBotGroupPolicySchema,
|
|
662
|
+
systemPrompt: z.string().optional(),
|
|
663
|
+
markdownSupport: z.boolean().optional(),
|
|
664
|
+
voiceDirectUploadFormats: z.array(z.string()).optional(),
|
|
665
|
+
audioFormatPolicy: AudioFormatPolicySchema,
|
|
666
|
+
urlDirectUpload: z.boolean().optional(),
|
|
667
|
+
upgradeUrl: z.string().optional(),
|
|
668
|
+
upgradeMode: z.enum(["doc", "hot-reload"]).optional(),
|
|
669
|
+
streaming: QQBotStreamingSchema,
|
|
670
|
+
execApprovals: QQBotExecApprovalsSchema
|
|
671
|
+
}).passthrough();
|
|
672
|
+
const qqbotChannelConfigSchema = buildChannelConfigSchema(QQBotAccountSchema.extend({
|
|
673
|
+
stt: QQBotSttSchema,
|
|
674
|
+
accounts: z.object({}).catchall(QQBotAccountSchema.passthrough()).optional(),
|
|
675
|
+
defaultAccount: z.string().optional()
|
|
676
|
+
}).passthrough());
|
|
677
|
+
//#endregion
|
|
678
|
+
export { qqbotSetupAdapterShared as a, listQQBotAccountIds as c, DEFAULT_ACCOUNT_ID$2 as d, resolveAccountBase as f, setBridgeLogger as h, qqbotMeta as i, resolveDefaultQQBotAccountId as l, getBridgeLogger as m, qqbotSetupWizard as n, DEFAULT_ACCOUNT_ID$1 as o, ensurePlatformAdapter as p, qqbotConfigAdapter as r, applyQQBotAccountConfig as s, qqbotChannelConfigSchema as t, resolveQQBotAccount as u };
|