@tencent-connect/openclaw-qqbot 1.7.1 → 2.0.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/README.md +206 -4
- package/README.zh.md +207 -4
- package/bin/qqbot-cli.js +5 -2
- package/dist/index.cjs +15549 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +890 -0
- package/index.ts +56 -18
- package/openclaw.plugin.json +19 -3
- package/package.json +14 -23
- package/preload.cjs +17 -14
- package/scripts/link-sdk-core.cjs +133 -50
- package/scripts/upgrade-via-npm.sh +400 -73
- package/scripts/upgrade-via-source.sh +126 -89
- package/skills/qqbot-channel/SKILL.md +34 -12
- package/src/adapter/contract.ts +69 -0
- package/src/adapter/gateway.ts +61 -0
- package/src/adapter/index.ts +15 -0
- package/src/adapter/lint.ts +144 -0
- package/src/adapter/media.ts +112 -0
- package/src/adapter/pairing.ts +99 -0
- package/src/adapter/resolve.ts +333 -0
- package/src/adapter/setup.ts +55 -0
- package/src/adapter/webhook.ts +248 -0
- package/src/adapter/workspace.ts +29 -0
- package/src/bot-instance.ts +60 -0
- package/src/channel.ts +202 -413
- package/src/commands/bot-approve.ts +143 -0
- package/src/commands/bot-clear-storage.ts +114 -0
- package/src/commands/bot-group-always.ts +62 -0
- package/src/commands/bot-help.ts +40 -0
- package/src/commands/bot-logs.ts +248 -0
- package/src/commands/bot-me.ts +18 -0
- package/src/commands/bot-pairing.ts +57 -0
- package/src/commands/bot-ping.ts +33 -0
- package/src/commands/bot-streaming.ts +55 -0
- package/src/commands/bot-upgrade.ts +56 -0
- package/src/commands/bot-version.ts +41 -0
- package/src/commands/config-util.ts +96 -0
- package/src/commands/index.ts +49 -0
- package/src/config.ts +59 -19
- package/src/dispatch/body-assembler.ts +356 -0
- package/src/dispatch/ctx-builder.ts +110 -0
- package/src/dispatch/dispatch.ts +352 -0
- package/src/dispatch/envelope-builder.ts +112 -0
- package/src/dispatch/index.ts +2 -0
- package/src/{approval-handler.ts → features/approval-handler.ts} +40 -94
- package/src/features/approval-utils.ts +41 -0
- package/src/features/credential-backup.ts +82 -0
- package/src/features/history-store.ts +19 -0
- package/src/features/msgid-cache.ts +55 -0
- package/src/features/onboarding.ts +38 -0
- package/src/{proactive.ts → features/proactive.ts} +68 -49
- package/src/features/ref-index-store.ts +282 -0
- package/src/{update-checker.ts → features/update-checker.ts} +15 -35
- package/src/gateway/event-handlers.ts +241 -0
- package/src/gateway/index.ts +3 -0
- package/src/gateway/lifecycle.ts +221 -0
- package/src/gateway/middleware-setup.ts +139 -0
- package/src/gateway/qqbot-gateway.ts +342 -0
- package/src/middleware/access-control.ts +130 -0
- package/src/middleware/attachment.ts +314 -0
- package/src/middleware/policy-injector.ts +66 -0
- package/src/openclaw-plugin-sdk.d.ts +81 -10
- package/src/outbound/cron-scheduler.ts +130 -0
- package/src/outbound/debounce.ts +102 -0
- package/src/outbound/deliver-pipeline.ts +235 -0
- package/src/outbound/image-size.ts +123 -0
- package/src/outbound/index.ts +3 -0
- package/src/outbound/local-file-router.ts +145 -0
- package/src/outbound/media-send.ts +377 -0
- package/src/outbound/outbound-service.ts +222 -0
- package/src/outbound/reply-limiter.ts +128 -0
- package/src/outbound/sanitize.ts +32 -0
- package/src/outbound/streaming-controller.ts +228 -0
- package/src/outbound/target.ts +63 -0
- package/src/outbound/tts-provider.ts +84 -0
- package/src/request-context.ts +9 -8
- package/src/runtime.ts +31 -6
- package/src/setup/account-key.ts +41 -0
- package/src/setup/finalize.ts +110 -0
- package/src/setup/login.ts +197 -0
- package/src/setup/surface.ts +40 -0
- package/src/tools/platform.ts +149 -0
- package/src/tools/remind.ts +1 -1
- package/src/types-augment.d.ts +54 -0
- package/src/types.ts +71 -8
- package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
- package/src/utils/mention.ts +52 -0
- package/src/utils/pkg-version.ts +84 -47
- package/src/utils/platform.ts +31 -7
- package/src/utils/plugin-logger.ts +104 -0
- package/src/utils/ssrf-guard.ts +32 -2
- package/src/utils/stt.ts +137 -0
- package/src/utils/voice-text.ts +61 -0
- package/tsconfig.json +2 -1
- package/tsup.config.ts +57 -0
- package/dist/index.d.ts +0 -17
- package/dist/index.js +0 -26
- package/dist/src/admin-resolver.d.ts +0 -33
- package/dist/src/admin-resolver.js +0 -157
- package/dist/src/api.d.ts +0 -307
- package/dist/src/api.js +0 -910
- package/dist/src/approval-handler.d.ts +0 -47
- package/dist/src/approval-handler.js +0 -372
- package/dist/src/channel.d.ts +0 -29
- package/dist/src/channel.js +0 -528
- package/dist/src/config.d.ts +0 -56
- package/dist/src/config.js +0 -278
- package/dist/src/credential-backup.d.ts +0 -31
- package/dist/src/credential-backup.js +0 -66
- package/dist/src/deliver-debounce.d.ts +0 -74
- package/dist/src/deliver-debounce.js +0 -174
- package/dist/src/gateway.d.ts +0 -18
- package/dist/src/gateway.js +0 -2063
- package/dist/src/group-history.d.ts +0 -136
- package/dist/src/group-history.js +0 -226
- package/dist/src/image-server.d.ts +0 -87
- package/dist/src/image-server.js +0 -570
- package/dist/src/inbound-attachments.d.ts +0 -60
- package/dist/src/inbound-attachments.js +0 -248
- package/dist/src/known-users.d.ts +0 -100
- package/dist/src/known-users.js +0 -263
- package/dist/src/message-gating.d.ts +0 -53
- package/dist/src/message-gating.js +0 -107
- package/dist/src/message-queue.d.ts +0 -91
- package/dist/src/message-queue.js +0 -257
- package/dist/src/onboarding.d.ts +0 -10
- package/dist/src/onboarding.js +0 -203
- package/dist/src/outbound-deliver.d.ts +0 -48
- package/dist/src/outbound-deliver.js +0 -392
- package/dist/src/outbound.d.ts +0 -220
- package/dist/src/outbound.js +0 -975
- package/dist/src/proactive.d.ts +0 -170
- package/dist/src/proactive.js +0 -399
- package/dist/src/ref-index-store.d.ts +0 -101
- package/dist/src/ref-index-store.js +0 -298
- package/dist/src/reply-dispatcher.d.ts +0 -35
- package/dist/src/reply-dispatcher.js +0 -311
- package/dist/src/request-context.d.ts +0 -25
- package/dist/src/request-context.js +0 -37
- package/dist/src/runtime.d.ts +0 -3
- package/dist/src/runtime.js +0 -13
- package/dist/src/session-store.d.ts +0 -52
- package/dist/src/session-store.js +0 -254
- package/dist/src/slash-commands.d.ts +0 -82
- package/dist/src/slash-commands.js +0 -2117
- package/dist/src/startup-greeting.d.ts +0 -30
- package/dist/src/startup-greeting.js +0 -97
- package/dist/src/streaming.d.ts +0 -247
- package/dist/src/streaming.js +0 -899
- package/dist/src/stt.d.ts +0 -21
- package/dist/src/stt.js +0 -70
- package/dist/src/tools/channel.d.ts +0 -16
- package/dist/src/tools/channel.js +0 -234
- package/dist/src/tools/remind.d.ts +0 -2
- package/dist/src/tools/remind.js +0 -256
- package/dist/src/types.d.ts +0 -443
- package/dist/src/types.js +0 -22
- package/dist/src/typing-keepalive.d.ts +0 -27
- package/dist/src/typing-keepalive.js +0 -64
- package/dist/src/update-checker.d.ts +0 -36
- package/dist/src/update-checker.js +0 -171
- package/dist/src/utils/audio-convert.d.ts +0 -98
- package/dist/src/utils/audio-convert.js +0 -755
- package/dist/src/utils/chunked-upload.d.ts +0 -68
- package/dist/src/utils/chunked-upload.js +0 -341
- package/dist/src/utils/file-utils.d.ts +0 -61
- package/dist/src/utils/file-utils.js +0 -172
- package/dist/src/utils/image-size.d.ts +0 -51
- package/dist/src/utils/image-size.js +0 -234
- package/dist/src/utils/media-send.d.ts +0 -158
- package/dist/src/utils/media-send.js +0 -502
- package/dist/src/utils/media-tags.d.ts +0 -14
- package/dist/src/utils/media-tags.js +0 -165
- package/dist/src/utils/payload.d.ts +0 -112
- package/dist/src/utils/payload.js +0 -186
- package/dist/src/utils/pkg-version.d.ts +0 -5
- package/dist/src/utils/pkg-version.js +0 -61
- package/dist/src/utils/platform.d.ts +0 -137
- package/dist/src/utils/platform.js +0 -390
- package/dist/src/utils/ssrf-guard.d.ts +0 -25
- package/dist/src/utils/ssrf-guard.js +0 -91
- package/dist/src/utils/text-parsing.d.ts +0 -31
- package/dist/src/utils/text-parsing.js +0 -74
- package/dist/src/utils/upload-cache.d.ts +0 -34
- package/dist/src/utils/upload-cache.js +0 -93
- package/node_modules/@eshaz/web-worker/LICENSE +0 -201
- package/node_modules/@eshaz/web-worker/README.md +0 -134
- package/node_modules/@eshaz/web-worker/browser.js +0 -17
- package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
- package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
- package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
- package/node_modules/@eshaz/web-worker/node.js +0 -223
- package/node_modules/@eshaz/web-worker/package.json +0 -54
- package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
- package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
- package/node_modules/mpg123-decoder/README.md +0 -265
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
- package/node_modules/mpg123-decoder/index.js +0 -8
- package/node_modules/mpg123-decoder/package.json +0 -58
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
- package/node_modules/mpg123-decoder/types.d.ts +0 -30
- package/node_modules/silk-wasm/LICENSE +0 -21
- package/node_modules/silk-wasm/README.md +0 -85
- package/node_modules/silk-wasm/lib/index.cjs +0 -16
- package/node_modules/silk-wasm/lib/index.d.ts +0 -70
- package/node_modules/silk-wasm/lib/index.mjs +0 -16
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
- package/node_modules/silk-wasm/package.json +0 -39
- package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
- package/node_modules/simple-yenc/.prettierignore +0 -1
- package/node_modules/simple-yenc/LICENSE +0 -7
- package/node_modules/simple-yenc/README.md +0 -163
- package/node_modules/simple-yenc/dist/esm.js +0 -1
- package/node_modules/simple-yenc/dist/index.js +0 -1
- package/node_modules/simple-yenc/package.json +0 -50
- package/node_modules/simple-yenc/rollup.config.js +0 -27
- package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -13
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -528
- package/node_modules/ws/lib/receiver.js +0 -706
- package/node_modules/ws/lib/sender.js +0 -602
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -554
- package/node_modules/ws/lib/websocket.js +0 -1393
- package/node_modules/ws/package.json +0 -69
- package/node_modules/ws/wrapper.mjs +0 -8
- package/scripts/postinstall-link-sdk.js +0 -126
- package/skills/qqbot-media/SKILL.md +0 -60
- package/src/admin-resolver.ts +0 -181
- package/src/api.ts +0 -1319
- package/src/credential-backup.ts +0 -72
- package/src/deliver-debounce.ts +0 -229
- package/src/gateway.ts +0 -2304
- package/src/group-history.ts +0 -328
- package/src/image-server.ts +0 -675
- package/src/inbound-attachments.ts +0 -321
- package/src/known-users.ts +0 -353
- package/src/message-gating.ts +0 -190
- package/src/message-queue.ts +0 -354
- package/src/onboarding.ts +0 -274
- package/src/outbound-deliver.ts +0 -486
- package/src/outbound.ts +0 -1179
- package/src/ref-index-store.ts +0 -412
- package/src/reply-dispatcher.ts +0 -334
- package/src/session-store.ts +0 -303
- package/src/slash-commands.ts +0 -2297
- package/src/startup-greeting.ts +0 -120
- package/src/streaming.ts +0 -1077
- package/src/stt.ts +0 -86
- package/src/tools/channel.ts +0 -281
- package/src/typing-keepalive.ts +0 -59
- package/src/utils/audio-convert.ts +0 -859
- package/src/utils/chunked-upload.ts +0 -483
- package/src/utils/file-utils.ts +0 -193
- package/src/utils/image-size.ts +0 -266
- package/src/utils/media-send.ts +0 -636
- package/src/utils/media-tags.ts +0 -183
- package/src/utils/payload.ts +0 -265
- package/src/utils/text-parsing.ts +0 -85
- package/src/utils/upload-cache.ts +0 -128
package/src/channel.ts
CHANGED
|
@@ -1,155 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Bot ChannelPlugin 定义
|
|
3
|
+
*
|
|
4
|
+
* 薄壳编排层 — 实现 OpenClaw ChannelPlugin 接口,
|
|
5
|
+
* 将各子模块(gateway/outbound/config/features)连接为完整通道插件。
|
|
6
|
+
*/
|
|
1
7
|
import {
|
|
2
8
|
type ChannelPlugin,
|
|
3
9
|
type OpenClawConfig,
|
|
4
10
|
applyAccountNameToChannelSection,
|
|
5
11
|
deleteAccountFromConfigSection,
|
|
6
12
|
setAccountEnabledInConfigSection,
|
|
7
|
-
} from
|
|
8
|
-
|
|
9
|
-
import type { ResolvedQQBotAccount } from "./types.js";
|
|
10
|
-
import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId, resolveRequireMention, resolveToolPolicy, resolveGroupConfig } from "./config.js";
|
|
11
|
-
import { sendText, sendMedia, resolveUserFacingMediaError } from "./outbound.js";
|
|
12
|
-
import { startGateway } from "./gateway.js";
|
|
13
|
-
import { qqbotOnboardingAdapter } from "./onboarding.js";
|
|
14
|
-
import { getQQBotRuntime } from "./runtime.js";
|
|
15
|
-
import { saveCredentialBackup, loadCredentialBackup } from "./credential-backup.js";
|
|
16
|
-
import { initApiConfig } from "./api.js";
|
|
17
|
-
import { getApprovalHandler } from "./approval-handler.js";
|
|
13
|
+
} from 'openclaw/plugin-sdk/core';
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
15
|
+
import type { ResolvedQQBotAccount } from './types.js';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_ACCOUNT_ID,
|
|
18
|
+
listQQBotAccountIds,
|
|
19
|
+
resolveQQBotAccount,
|
|
20
|
+
applyQQBotAccountConfig,
|
|
21
|
+
resolveDefaultQQBotAccountId,
|
|
22
|
+
resolveRequireMention,
|
|
23
|
+
resolveToolPolicy,
|
|
24
|
+
resolveGroupConfig,
|
|
25
|
+
} from './config.js';
|
|
26
|
+
import { getQQBotRuntime, tryGetQQBotRuntime } from './runtime.js';
|
|
27
|
+
import { getAdapters } from './adapter/resolve.js';
|
|
28
|
+
import { sendText, getGateway } from './outbound/outbound-service.js';
|
|
29
|
+
import { sendMedia } from './outbound/media-send.js';
|
|
30
|
+
import type { PluginLogger } from './utils/plugin-logger.js';
|
|
31
|
+
import { qqbotSetupWizard } from './setup/surface.js';
|
|
32
|
+
import { qqbotLogin, startQrLogin, waitQrLogin } from './setup/login.js';
|
|
33
|
+
import { normalizeTarget, isQQBotTarget } from './outbound/target.js';
|
|
34
|
+
import { sanitizeQQBotText } from './outbound/sanitize.js';
|
|
35
|
+
import { startAccountWithCredentialRecovery, logoutAndClearCredentials, stopAccountGracefully } from './gateway/lifecycle.js';
|
|
36
|
+
import { loadCredentialBackup } from './features/credential-backup.js';
|
|
37
|
+
import { isApprovalPayload, approvalStubs } from './features/approval-utils.js';
|
|
38
|
+
import { qqbotOnboardingAdapter } from './features/onboarding.js';
|
|
39
|
+
import { stripMentionText } from './utils/mention.js';
|
|
35
40
|
|
|
36
41
|
/** QQ Bot 单条消息文本长度上限 */
|
|
37
42
|
export const TEXT_CHUNK_LIMIT = 5000;
|
|
38
43
|
|
|
44
|
+
// ── GFM 表格检测 ──
|
|
45
|
+
|
|
46
|
+
/** GFM 表格数据行: | col1 | col2 | */
|
|
47
|
+
const GFM_TABLE_DATA_RE = /^\|.+\|.*\|/;
|
|
48
|
+
/** GFM 表格分隔行: |---|:---:|---| (1 个或多于 1 个破折号,支持对齐冒号) */
|
|
49
|
+
const GFM_TABLE_SEP_RE = /^\|[\s:-]+\|/;
|
|
50
|
+
|
|
39
51
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* 支持代码块自动关闭/重开、括号感知等
|
|
52
|
+
* 判断一行是否为 GFM 表格行(数据行或分隔行)。
|
|
53
|
+
* 保障 table-aware chunker 不会在表格内部切分。
|
|
43
54
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return runtime.channel.text.chunkMarkdownText(text, limit);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function buildChannelMediaError(result: Parameters<typeof resolveUserFacingMediaError>[0]): Error {
|
|
50
|
-
const err = new Error(resolveUserFacingMediaError(result));
|
|
51
|
-
if (result.errorCode) {
|
|
52
|
-
(err as Error & { code?: string }).code = result.errorCode;
|
|
53
|
-
}
|
|
54
|
-
if (result.qqBizCode !== undefined) {
|
|
55
|
-
(err as Error & { qqBizCode?: number }).qqBizCode = result.qqBizCode;
|
|
56
|
-
}
|
|
57
|
-
return err;
|
|
55
|
+
function isGfmTableLine(line: string): boolean {
|
|
56
|
+
return GFM_TABLE_DATA_RE.test(line) || GFM_TABLE_SEP_RE.test(line);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
|
|
61
|
-
id:
|
|
60
|
+
id: 'qqbot',
|
|
62
61
|
meta: {
|
|
63
|
-
id:
|
|
64
|
-
label:
|
|
65
|
-
selectionLabel:
|
|
66
|
-
docsPath:
|
|
67
|
-
blurb:
|
|
62
|
+
id: 'qqbot',
|
|
63
|
+
label: 'QQ Bot',
|
|
64
|
+
selectionLabel: 'QQ Bot',
|
|
65
|
+
docsPath: '/docs/channels/qqbot',
|
|
66
|
+
blurb: 'Connect to QQ via official QQ Bot API',
|
|
68
67
|
order: 50,
|
|
69
68
|
},
|
|
70
69
|
capabilities: {
|
|
71
|
-
chatTypes: [
|
|
70
|
+
chatTypes: ['direct', 'group'],
|
|
72
71
|
media: true,
|
|
73
72
|
reactions: false,
|
|
74
73
|
threads: false,
|
|
75
|
-
|
|
76
|
-
* blockStreaming: true 表示该 Channel 支持块流式
|
|
77
|
-
* 框架会收集流式响应,然后通过 deliver 回调发送
|
|
78
|
-
*/
|
|
79
|
-
blockStreaming: true,
|
|
74
|
+
blockStreaming: false,
|
|
80
75
|
},
|
|
81
|
-
|
|
76
|
+
gatewayMethods: ['web.login.start', 'web.login.wait'],
|
|
77
|
+
reload: { configPrefixes: ['channels.qqbot'] },
|
|
82
78
|
|
|
83
|
-
//
|
|
79
|
+
// ── 群消息策略 ──
|
|
84
80
|
groups: {
|
|
85
|
-
/** 是否需要 @机器人才响应 */
|
|
86
81
|
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
|
87
82
|
if (!groupId) return undefined;
|
|
88
83
|
return resolveRequireMention(cfg, groupId, accountId ?? undefined);
|
|
89
84
|
},
|
|
90
|
-
|
|
91
|
-
/** 群聊工具范围 */
|
|
92
85
|
resolveToolPolicy: ({ cfg, accountId, groupId }) => {
|
|
93
86
|
if (!groupId) return undefined;
|
|
94
87
|
const policy = resolveToolPolicy(cfg, groupId, accountId ?? undefined);
|
|
95
|
-
|
|
96
|
-
if (policy ===
|
|
97
|
-
if (policy === "none") return { allow: [], deny: ["*"] };
|
|
98
|
-
// restricted: 默认空 allow(框架会使用内置 restricted 列表)
|
|
88
|
+
if (policy === 'full') return undefined;
|
|
89
|
+
if (policy === 'none') return { allow: [], deny: ['*'] };
|
|
99
90
|
return { allow: [] };
|
|
100
91
|
},
|
|
101
|
-
|
|
102
|
-
/** QQ Bot 平台特有的群聊行为提示 */
|
|
103
92
|
resolveGroupIntroHint: ({ cfg, accountId, groupId }) => {
|
|
104
93
|
if (!groupId) return undefined;
|
|
105
94
|
const groupCfg = resolveGroupConfig(cfg, groupId, accountId ?? undefined);
|
|
106
|
-
|
|
107
|
-
if (groupCfg.name) {
|
|
108
|
-
hints.push(`当前群: ${groupCfg.name}`);
|
|
109
|
-
}
|
|
110
|
-
// bot 互聊防护、@状态行为指引在 gateway.ts 动态注入
|
|
111
|
-
return hints.join(" ") || undefined;
|
|
95
|
+
return groupCfg.name ? `当前群: ${groupCfg.name}` : undefined;
|
|
112
96
|
},
|
|
113
97
|
},
|
|
114
98
|
|
|
115
|
-
//
|
|
99
|
+
// ── @mention 检测与清理 ──
|
|
116
100
|
mentions: {
|
|
117
|
-
/** 清理 @mention 文本(SDK ChannelMentionAdapter 接口) */
|
|
118
101
|
stripMentions: ({ text, ctx }) => {
|
|
119
|
-
const mentions = (ctx as any)?.mentions
|
|
102
|
+
const mentions = (ctx as any)?.mentions;
|
|
120
103
|
return stripMentionText(text, mentions);
|
|
121
104
|
},
|
|
122
105
|
},
|
|
123
|
-
|
|
124
|
-
// @ts-ignore onboarding
|
|
106
|
+
|
|
107
|
+
// @ts-ignore onboarding 兼容
|
|
125
108
|
onboarding: qqbotOnboardingAdapter,
|
|
126
109
|
|
|
110
|
+
// ── 配置管理 ──
|
|
127
111
|
config: {
|
|
128
112
|
listAccountIds: (cfg) => listQQBotAccountIds(cfg),
|
|
129
113
|
resolveAccount: (cfg, accountId) => resolveQQBotAccount(cfg, accountId),
|
|
130
114
|
defaultAccountId: (cfg) => resolveDefaultQQBotAccountId(cfg),
|
|
131
|
-
// 新增:设置账户启用状态
|
|
132
115
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
133
|
-
setAccountEnabledInConfigSection({
|
|
134
|
-
cfg,
|
|
135
|
-
sectionKey: "qqbot",
|
|
136
|
-
accountId,
|
|
137
|
-
enabled,
|
|
138
|
-
allowTopLevel: true,
|
|
139
|
-
}),
|
|
140
|
-
// 新增:删除账户
|
|
116
|
+
setAccountEnabledInConfigSection({ cfg, sectionKey: 'qqbot', accountId, enabled, allowTopLevel: true }),
|
|
141
117
|
deleteAccount: ({ cfg, accountId }) =>
|
|
142
118
|
deleteAccountFromConfigSection({
|
|
143
|
-
cfg,
|
|
144
|
-
|
|
145
|
-
accountId,
|
|
146
|
-
clearBaseFields: ["appId", "clientSecret", "clientSecretFile", "name"],
|
|
119
|
+
cfg, sectionKey: 'qqbot', accountId,
|
|
120
|
+
clearBaseFields: ['appId', 'clientSecret', 'clientSecretFile', 'name'],
|
|
147
121
|
}),
|
|
148
122
|
isConfigured: (account) => {
|
|
149
123
|
if (account?.appId && account?.clientSecret) return true;
|
|
150
|
-
|
|
151
|
-
const backup = loadCredentialBackup(account?.accountId);
|
|
152
|
-
return backup !== null;
|
|
124
|
+
return loadCredentialBackup(account?.accountId) !== null;
|
|
153
125
|
},
|
|
154
126
|
describeAccount: (account) => ({
|
|
155
127
|
accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
@@ -158,275 +130,160 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
|
|
|
158
130
|
configured: Boolean(account?.appId && account?.clientSecret),
|
|
159
131
|
tokenSource: account?.secretSource,
|
|
160
132
|
}),
|
|
161
|
-
// 关键:解析 allowFrom 配置,用于命令授权
|
|
162
133
|
resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) => {
|
|
163
134
|
const account = resolveQQBotAccount(cfg, accountId ?? undefined);
|
|
164
|
-
|
|
165
|
-
console.log(`[qqbot] resolveAllowFrom: accountId=${accountId}, allowFrom=${JSON.stringify(allowFrom)}`);
|
|
166
|
-
return allowFrom.map((entry: string | number) => String(entry)) as (string | number)[];
|
|
135
|
+
return (account.config?.allowFrom ?? []).map((e: string | number) => String(e)) as (string | number)[];
|
|
167
136
|
},
|
|
168
|
-
|
|
169
|
-
formatAllowFrom: ({ allowFrom }: { allowFrom: Array<string | number> }) =>
|
|
137
|
+
formatAllowFrom: ({ allowFrom }: { allowFrom: (string | number)[] }) =>
|
|
170
138
|
allowFrom
|
|
171
|
-
.map((
|
|
139
|
+
.map((e: string | number) => String(e).trim())
|
|
172
140
|
.filter(Boolean)
|
|
173
|
-
.map((
|
|
174
|
-
.map((entry: string) => entry.toUpperCase()), // QQ openid 是大写的
|
|
141
|
+
.map((e: string) => e.replace(/^qqbot:/i, '').toUpperCase()),
|
|
175
142
|
},
|
|
143
|
+
|
|
144
|
+
// ── Setup ──
|
|
176
145
|
setup: {
|
|
177
|
-
// 新增:规范化账户 ID
|
|
178
146
|
resolveAccountId: ({ accountId }) => accountId?.trim().toLowerCase() || DEFAULT_ACCOUNT_ID,
|
|
179
|
-
// 新增:应用账户名称
|
|
180
147
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
181
|
-
applyAccountNameToChannelSection({
|
|
182
|
-
cfg,
|
|
183
|
-
channelKey: "qqbot",
|
|
184
|
-
accountId,
|
|
185
|
-
name,
|
|
186
|
-
}),
|
|
148
|
+
applyAccountNameToChannelSection({ cfg, channelKey: 'qqbot', accountId, name }),
|
|
187
149
|
validateInput: ({ input }) => {
|
|
188
150
|
if (!input.token && !input.tokenFile && !input.useEnv) {
|
|
189
|
-
return
|
|
151
|
+
return 'QQBot requires --token (format: appId:clientSecret) or --use-env';
|
|
190
152
|
}
|
|
191
153
|
return null;
|
|
192
154
|
},
|
|
193
155
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
194
|
-
let appId =
|
|
195
|
-
let clientSecret =
|
|
196
|
-
|
|
156
|
+
let appId = '';
|
|
157
|
+
let clientSecret = '';
|
|
197
158
|
if (input.token) {
|
|
198
|
-
const parts = input.token.split(
|
|
199
|
-
if (parts.length === 2) {
|
|
200
|
-
appId = parts[0];
|
|
201
|
-
clientSecret = parts[1];
|
|
202
|
-
}
|
|
159
|
+
const parts = input.token.split(':');
|
|
160
|
+
if (parts.length === 2) { appId = parts[0]; clientSecret = parts[1]; }
|
|
203
161
|
}
|
|
204
|
-
|
|
205
162
|
return applyQQBotAccountConfig(cfg, accountId, {
|
|
206
|
-
appId,
|
|
207
|
-
clientSecret,
|
|
163
|
+
appId, clientSecret,
|
|
208
164
|
clientSecretFile: input.tokenFile,
|
|
209
165
|
name: input.name,
|
|
210
|
-
imageServerBaseUrl: (input as Record<string, unknown>).imageServerBaseUrl as string | undefined,
|
|
211
166
|
}) as OpenClawConfig;
|
|
212
167
|
},
|
|
213
168
|
},
|
|
214
|
-
// Messaging 配置:用于解析目标地址
|
|
215
|
-
messaging: {
|
|
216
|
-
/**
|
|
217
|
-
* 规范化目标地址
|
|
218
|
-
* 支持以下格式:
|
|
219
|
-
* - qqbot:c2c:openid -> 私聊
|
|
220
|
-
* - qqbot:group:groupid -> 群聊
|
|
221
|
-
* - qqbot:channel:channelid -> 频道
|
|
222
|
-
* - c2c:openid -> 私聊
|
|
223
|
-
* - group:groupid -> 群聊
|
|
224
|
-
* - channel:channelid -> 频道
|
|
225
|
-
* - 纯 openid(32位十六进制)-> 私聊
|
|
226
|
-
*/
|
|
227
|
-
normalizeTarget: (target: string): string | undefined => {
|
|
228
|
-
// 去掉 qqbot: 前缀(如果有)
|
|
229
|
-
const id = target.replace(/^qqbot:/i, "");
|
|
230
|
-
|
|
231
|
-
// 检查是否是已知格式
|
|
232
|
-
if (id.startsWith("c2c:") || id.startsWith("group:") || id.startsWith("channel:")) {
|
|
233
|
-
return `qqbot:${id}`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// 检查是否是纯 openid(32位十六进制,不带连字符)
|
|
237
|
-
// QQ Bot OpenID 格式类似: 207A5B8339D01F6582911C014668B77B
|
|
238
|
-
const openIdHexPattern = /^[0-9a-fA-F]{32}$/;
|
|
239
|
-
if (openIdHexPattern.test(id)) {
|
|
240
|
-
return `qqbot:c2c:${id}`;
|
|
241
|
-
}
|
|
242
169
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// 不认识的格式,返回 undefined 让核心使用原始值
|
|
250
|
-
return undefined;
|
|
251
|
-
},
|
|
252
|
-
/**
|
|
253
|
-
* 目标解析器配置
|
|
254
|
-
* 用于判断一个目标 ID 是否看起来像 QQ Bot 的格式
|
|
255
|
-
*/
|
|
170
|
+
setupWizard: qqbotSetupWizard,
|
|
171
|
+
|
|
172
|
+
// ── Messaging ──
|
|
173
|
+
messaging: {
|
|
174
|
+
normalizeTarget,
|
|
256
175
|
targetResolver: {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
176
|
+
looksLikeId: isQQBotTarget,
|
|
177
|
+
hint: 'QQ Bot 目标格式: qqbot:c2c:openid (私聊) 或 qqbot:group:groupid (群聊)',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// ── 出站 ──
|
|
182
|
+
outbound: {
|
|
183
|
+
deliveryMode: 'direct' as const,
|
|
184
|
+
sanitizeText: ({ text }: { text: string; payload: any }) => sanitizeQQBotText(text),
|
|
185
|
+
chunker: (text, limit) => {
|
|
186
|
+
const adapters = getAdapters(getQQBotRuntime());
|
|
187
|
+
if (adapters.chunkMarkdownText) return adapters.chunkMarkdownText(text, limit);
|
|
188
|
+
// fallback(低版本降级): 按换行边界切分,保留 Markdown 表格完整性
|
|
189
|
+
const lines = text.split('\n');
|
|
190
|
+
const chunks: string[] = [];
|
|
191
|
+
let current = '';
|
|
192
|
+
let tableBuffer: string[] = [];
|
|
193
|
+
|
|
194
|
+
const flushTable = () => {
|
|
195
|
+
if (tableBuffer.length === 0) return;
|
|
196
|
+
const tableBlock = tableBuffer.join('\n');
|
|
197
|
+
const candidate = current ? `${current}\n${tableBlock}` : tableBlock;
|
|
198
|
+
if (candidate.length > limit && current) {
|
|
199
|
+
chunks.push(current);
|
|
200
|
+
current = tableBlock;
|
|
201
|
+
} else {
|
|
202
|
+
current = candidate;
|
|
272
203
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
204
|
+
tableBuffer = [];
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
if (isGfmTableLine(line)) {
|
|
209
|
+
tableBuffer.push(line);
|
|
210
|
+
continue;
|
|
276
211
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
212
|
+
|
|
213
|
+
// 遇到非表格行,先刷新缓冲的表格
|
|
214
|
+
flushTable();
|
|
215
|
+
|
|
216
|
+
const candidate = current ? `${current}\n${line}` : line;
|
|
217
|
+
if (candidate.length > limit && current) {
|
|
218
|
+
chunks.push(current);
|
|
219
|
+
current = line;
|
|
220
|
+
} else {
|
|
221
|
+
current = candidate;
|
|
280
222
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
223
|
+
}
|
|
224
|
+
// 处理末尾的表格缓冲
|
|
225
|
+
flushTable();
|
|
226
|
+
if (current) chunks.push(current);
|
|
227
|
+
return chunks.length > 0 ? chunks : [text];
|
|
286
228
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
291
|
-
chunkerMode: "markdown",
|
|
292
|
-
textChunkLimit: 5000,
|
|
293
|
-
// 3.31+ outbound 路径:dispatch-from-config → shouldSuppressLocalExecApprovalPrompt → outbound.shouldSuppressLocalPayloadPrompt
|
|
294
|
-
shouldSuppressLocalPayloadPrompt: ({ accountId, payload }: any) =>
|
|
295
|
-
getApprovalHandler(accountId ?? "") != null &&
|
|
296
|
-
isApprovalPayload(payload),
|
|
229
|
+
chunkerMode: 'markdown',
|
|
230
|
+
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
231
|
+
shouldSuppressLocalPayloadPrompt: ({ payload }: any) => isApprovalPayload(payload),
|
|
297
232
|
sendText: async ({ to, text, accountId, replyToId, cfg }) => {
|
|
298
|
-
console.log(`[qqbot:channel] sendText called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, text.length=${text?.length ?? 0}`);
|
|
299
|
-
console.log(`[qqbot:channel] sendText text preview: ${text?.slice(0, 100)}${(text?.length ?? 0) > 100 ? "..." : ""}`);
|
|
300
233
|
const account = resolveQQBotAccount(cfg, accountId ?? undefined);
|
|
301
|
-
|
|
302
|
-
|
|
234
|
+
const outLog = createOutLog(account.accountId);
|
|
235
|
+
outLog.debug(`sendText to=${to} len=${text.length} replyTo=${replyToId ?? '-'}`);
|
|
303
236
|
const result = await sendText({ to, text, accountId, replyToId, account });
|
|
304
|
-
console.log(`[qqbot:channel] sendText result: messageId=${result.messageId}, error=${result.error ?? "none"}`);
|
|
305
237
|
if (result.error) throw new Error(result.error);
|
|
306
|
-
return {
|
|
307
|
-
channel: "qqbot" as const,
|
|
308
|
-
messageId: result.messageId ?? "",
|
|
309
|
-
};
|
|
238
|
+
return { channel: 'qqbot' as const, messageId: result.messageId ?? '' };
|
|
310
239
|
},
|
|
311
240
|
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
241
|
+
const resolvedAccountId = accountId ?? resolveDefaultQQBotAccountId(cfg);
|
|
242
|
+
const outLog = createOutLog(resolvedAccountId);
|
|
243
|
+
outLog.debug(`sendMedia to=${to} url=${mediaUrl?.slice(0, 80)} len=${text?.length ?? 0} replyTo=${replyToId ?? '-'}`);
|
|
244
|
+
const result = await sendMedia({
|
|
245
|
+
to,
|
|
246
|
+
source: mediaUrl ?? '',
|
|
247
|
+
text,
|
|
248
|
+
replyToId,
|
|
249
|
+
accountId: resolvedAccountId,
|
|
250
|
+
log: outLog,
|
|
251
|
+
agentId: resolveMCPAgentId(to, resolvedAccountId, cfg, outLog),
|
|
252
|
+
});
|
|
323
253
|
if (result.error) {
|
|
324
|
-
|
|
254
|
+
outLog.error(`sendMedia failed: ${result.error}`);
|
|
255
|
+
throw new Error(result.error);
|
|
325
256
|
}
|
|
326
|
-
return {
|
|
327
|
-
channel: "qqbot" as const,
|
|
328
|
-
messageId: result.messageId ?? "",
|
|
329
|
-
};
|
|
257
|
+
return { channel: 'qqbot' as const, messageId: result.messageId ?? '' };
|
|
330
258
|
},
|
|
331
259
|
},
|
|
332
|
-
gateway: {
|
|
333
|
-
startAccount: async (ctx) => {
|
|
334
|
-
let { account } = ctx;
|
|
335
|
-
const { abortSignal, log, cfg } = ctx;
|
|
336
|
-
|
|
337
|
-
// 凭证恢复:如果 appId/secret 为空(热更新打断可能导致配置丢失),尝试从暂存文件恢复
|
|
338
|
-
if (!account.appId || !account.clientSecret) {
|
|
339
|
-
const backup = loadCredentialBackup(account.accountId);
|
|
340
|
-
if (backup) {
|
|
341
|
-
log?.info(`[qqbot:${account.accountId}] 配置中凭证为空,从暂存文件恢复 (appId=${backup.appId}, savedAt=${backup.savedAt})`);
|
|
342
|
-
try {
|
|
343
|
-
const runtime = getQQBotRuntime();
|
|
344
|
-
const restoredCfg = applyQQBotAccountConfig(cfg, account.accountId, {
|
|
345
|
-
appId: backup.appId,
|
|
346
|
-
clientSecret: backup.clientSecret,
|
|
347
|
-
});
|
|
348
|
-
const configApi = runtime.config as { writeConfigFile: (cfg: unknown) => Promise<void> };
|
|
349
|
-
await configApi.writeConfigFile(restoredCfg);
|
|
350
|
-
// 重新解析 account 以获取恢复后的值
|
|
351
|
-
account = resolveQQBotAccount(restoredCfg, account.accountId);
|
|
352
|
-
log?.info(`[qqbot:${account.accountId}] 凭证已恢复`);
|
|
353
|
-
} catch (e) {
|
|
354
|
-
log?.error(`[qqbot:${account.accountId}] 凭证恢复失败: ${e}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
log?.info(`[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`);
|
|
360
|
-
console.log(`[qqbot:channel] startAccount: accountId=${account.accountId}, appId=${account.appId}, secretSource=${account.secretSource}`);
|
|
361
260
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// 启动成功,保存凭证快照供后续恢复使用
|
|
370
|
-
saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
|
|
371
|
-
ctx.setStatus({
|
|
372
|
-
...ctx.getStatus(),
|
|
373
|
-
running: true,
|
|
374
|
-
connected: true,
|
|
375
|
-
lastConnectedAt: Date.now(),
|
|
376
|
-
});
|
|
377
|
-
},
|
|
378
|
-
onError: (error) => {
|
|
379
|
-
log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
|
|
380
|
-
ctx.setStatus({
|
|
381
|
-
...ctx.getStatus(),
|
|
382
|
-
lastError: error.message,
|
|
383
|
-
});
|
|
384
|
-
},
|
|
261
|
+
// ── 网关 ──
|
|
262
|
+
gateway: {
|
|
263
|
+
startAccount: (ctx) => startAccountWithCredentialRecovery(ctx),
|
|
264
|
+
stopAccount: async (ctx: { accountId: string; log?: any }) => {
|
|
265
|
+
await stopAccountGracefully({
|
|
266
|
+
accountId: ctx.accountId,
|
|
267
|
+
log: ctx.log,
|
|
385
268
|
});
|
|
386
269
|
},
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
let changed = false;
|
|
393
|
-
|
|
394
|
-
if (nextQQBot) {
|
|
395
|
-
const qqbot = nextQQBot as Record<string, unknown>;
|
|
396
|
-
if (accountId === DEFAULT_ACCOUNT_ID && qqbot.clientSecret) {
|
|
397
|
-
delete qqbot.clientSecret;
|
|
398
|
-
cleared = true;
|
|
399
|
-
changed = true;
|
|
400
|
-
}
|
|
401
|
-
const accounts = qqbot.accounts as Record<string, Record<string, unknown>> | undefined;
|
|
402
|
-
if (accounts && accountId in accounts) {
|
|
403
|
-
const entry = accounts[accountId] as Record<string, unknown> | undefined;
|
|
404
|
-
if (entry && "clientSecret" in entry) {
|
|
405
|
-
delete entry.clientSecret;
|
|
406
|
-
cleared = true;
|
|
407
|
-
changed = true;
|
|
408
|
-
}
|
|
409
|
-
if (entry && Object.keys(entry).length === 0) {
|
|
410
|
-
delete accounts[accountId];
|
|
411
|
-
changed = true;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (changed && nextQQBot) {
|
|
417
|
-
nextCfg.channels = { ...nextCfg.channels, qqbot: nextQQBot };
|
|
418
|
-
const runtime = getQQBotRuntime();
|
|
419
|
-
const configApi = runtime.config as { writeConfigFile: (cfg: OpenClawConfig) => Promise<void> };
|
|
420
|
-
await configApi.writeConfigFile(nextCfg);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const resolved = resolveQQBotAccount(changed ? nextCfg : cfg, accountId);
|
|
424
|
-
const loggedOut = resolved.secretSource === "none";
|
|
425
|
-
const envToken = Boolean(process.env.QQBOT_CLIENT_SECRET);
|
|
426
|
-
|
|
427
|
-
return { ok: true, cleared, envToken, loggedOut };
|
|
270
|
+
logoutAccount: (params) => logoutAndClearCredentials(params),
|
|
271
|
+
loginWithQrStart: async ({ accountId }: { accountId?: string }) => startQrLogin(accountId),
|
|
272
|
+
loginWithQrWait: async ({ accountId }: { accountId?: string }) => {
|
|
273
|
+
const result = await waitQrLogin(accountId);
|
|
274
|
+
return { connected: result.connected, message: result.message };
|
|
428
275
|
},
|
|
429
276
|
},
|
|
277
|
+
|
|
278
|
+
// ── 登录认证 ──
|
|
279
|
+
auth: {
|
|
280
|
+
login: qqbotLogin as any,
|
|
281
|
+
// 审批权限(从 approvalStubs 迁移)
|
|
282
|
+
authorizeActorAction: () => ({ authorized: true } as const),
|
|
283
|
+
getActionAvailabilityState: () => ({ kind: 'enabled' as const } as const),
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// ── 状态 ──
|
|
430
287
|
status: {
|
|
431
288
|
defaultRuntime: {
|
|
432
289
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
@@ -437,10 +294,9 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
|
|
|
437
294
|
lastInboundAt: null,
|
|
438
295
|
lastOutboundAt: null,
|
|
439
296
|
},
|
|
440
|
-
// 新增:构建通道摘要
|
|
441
297
|
buildChannelSummary: ({ snapshot }) => ({
|
|
442
298
|
configured: snapshot.configured ?? false,
|
|
443
|
-
tokenSource: snapshot.tokenSource ??
|
|
299
|
+
tokenSource: snapshot.tokenSource ?? 'none',
|
|
444
300
|
running: snapshot.running ?? false,
|
|
445
301
|
connected: snapshot.connected ?? false,
|
|
446
302
|
lastConnectedAt: snapshot.lastConnectedAt ?? null,
|
|
@@ -460,100 +316,33 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
|
|
|
460
316
|
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
461
317
|
}),
|
|
462
318
|
},
|
|
463
|
-
// QQBot approval-handler 通过独立 WS 连接自行处理 exec + plugin 审批消息投递(带 Inline Keyboard),
|
|
464
|
-
// 完全屏蔽框架 Forwarder 的纯文本通知。
|
|
465
|
-
//
|
|
466
|
-
// ── 3.28 扁平结构 ──
|
|
467
|
-
execApprovals: {
|
|
468
|
-
// 3.28 框架通过此方法判断 channel 是否支持审批
|
|
469
|
-
getInitiatingSurfaceState: ({ accountId }: { cfg: any; accountId?: string | null }) => {
|
|
470
|
-
return getApprovalHandler(accountId ?? "") != null
|
|
471
|
-
? { kind: "enabled" as const }
|
|
472
|
-
: { kind: "disabled" as const };
|
|
473
|
-
},
|
|
474
|
-
shouldSuppressForwardingFallback: (...args: any[]) => {
|
|
475
|
-
console.log("[QQBot] shouldSuppressForwardingFallback called", JSON.stringify(args?.[0]?.target ?? null));
|
|
476
|
-
return true;
|
|
477
|
-
},
|
|
478
|
-
shouldSuppressLocalPrompt: ({ accountId, payload }: any) =>
|
|
479
|
-
getApprovalHandler(accountId ?? "") != null &&
|
|
480
|
-
isApprovalPayload(payload),
|
|
481
|
-
buildPendingPayload: () => null,
|
|
482
|
-
buildResolvedPayload: () => null,
|
|
483
|
-
},
|
|
484
|
-
// ── 3.31+ 嵌套结构 ──
|
|
485
|
-
// auth 和 approvals 是 ChannelPlugin 顶层平级字段
|
|
486
|
-
//
|
|
487
|
-
// QQBot 审批模型:
|
|
488
|
-
// - QQBotApprovalHandler 通过独立 WS 自行投递带 Inline Keyboard 的审批消息
|
|
489
|
-
// - 用户点击按钮 → INTERACTION_CREATE → resolveApproval → gateway RPC
|
|
490
|
-
// - /approve 文本命令作为 URGENT_COMMAND 直接入队交给框架处理
|
|
491
|
-
auth: {
|
|
492
|
-
authorizeActorAction: () => ({ authorized: true }),
|
|
493
|
-
getActionAvailabilityState: ({ accountId }: {
|
|
494
|
-
cfg: any; accountId?: string | null; action: "approve";
|
|
495
|
-
}) => {
|
|
496
|
-
return getApprovalHandler(accountId ?? "") != null
|
|
497
|
-
? { kind: "enabled" as const }
|
|
498
|
-
: { kind: "disabled" as const };
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
approvals: {
|
|
502
|
-
delivery: {
|
|
503
|
-
hasConfiguredDmRoute: () => true,
|
|
504
|
-
shouldSuppressForwardingFallback: () => true,
|
|
505
|
-
},
|
|
506
|
-
render: {
|
|
507
|
-
exec: {
|
|
508
|
-
buildPendingPayload: () => null,
|
|
509
|
-
buildResolvedPayload: () => null,
|
|
510
|
-
},
|
|
511
|
-
plugin: {
|
|
512
|
-
buildPendingPayload: () => null,
|
|
513
|
-
buildResolvedPayload: () => null,
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
};
|
|
518
319
|
|
|
519
|
-
//
|
|
320
|
+
// ── 审批(stub — 实际由 features/approval-handler 处理)──
|
|
321
|
+
...approvalStubs,
|
|
322
|
+
};
|
|
520
323
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
return cleaned;
|
|
324
|
+
function resolveMCPAgentId(to: string, accountId: string, cfg: unknown, log?: PluginLogger): string | undefined {
|
|
325
|
+
try {
|
|
326
|
+
const parts = to.split(':');
|
|
327
|
+
const scope = parts[1];
|
|
328
|
+
const peerId = parts[2];
|
|
329
|
+
if (!scope || !peerId) return undefined;
|
|
330
|
+
const rt = tryGetQQBotRuntime();
|
|
331
|
+
if (!rt) return undefined;
|
|
332
|
+
const route = getAdapters(rt).resolveAgentRoute?.({
|
|
333
|
+
cfg, channel: 'qqbot', accountId,
|
|
334
|
+
peer: { kind: scope === 'group' ? 'group' : 'direct', id: peerId },
|
|
335
|
+
});
|
|
336
|
+
log?.debug(`resolveMCPAgentId to=${to} => agentId=${route?.agentId ?? 'none'}`);
|
|
337
|
+
return route?.agentId;
|
|
338
|
+
} catch { return undefined; }
|
|
538
339
|
}
|
|
539
340
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
mentions?: Array<{ is_you?: boolean }>;
|
|
544
|
-
content?: string;
|
|
545
|
-
mentionPatterns?: string[];
|
|
546
|
-
}): boolean {
|
|
547
|
-
if (mentions?.some((m) => m.is_you)) return true;
|
|
548
|
-
if (eventType === "GROUP_AT_MESSAGE_CREATE") return true;
|
|
549
|
-
if (mentionPatterns?.length && content) {
|
|
550
|
-
for (const pattern of mentionPatterns) {
|
|
551
|
-
try {
|
|
552
|
-
if (new RegExp(pattern, "i").test(content)) return true;
|
|
553
|
-
} catch {
|
|
554
|
-
// 无效正则,跳过
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return false;
|
|
341
|
+
function createOutLog(accountId: string): PluginLogger {
|
|
342
|
+
const gwLog = getGateway(accountId)?.log;
|
|
343
|
+
return gwLog?.child('outbound') ?? ({} as PluginLogger);
|
|
559
344
|
}
|
|
345
|
+
|
|
346
|
+
// Re-export for backward compatibility
|
|
347
|
+
export { stripMentionText } from './utils/mention.js';
|
|
348
|
+
export { detectWasMentioned } from './utils/mention.js';
|