@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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup 工具动态加载(plugin-sdk/setup + plugin-sdk/setup-tools)
|
|
3
|
+
*
|
|
4
|
+
* 新版本 openclaw 有这些导出,旧版本可能缺失。
|
|
5
|
+
* 不可用时提供最小降级实现。
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
|
|
9
|
+
const req = createRequire(__filename);
|
|
10
|
+
|
|
11
|
+
let _setup: typeof import('openclaw/plugin-sdk/setup') | null | undefined;
|
|
12
|
+
let _tools: typeof import('openclaw/plugin-sdk/setup-tools') | null | undefined;
|
|
13
|
+
|
|
14
|
+
function loadSetup() {
|
|
15
|
+
if (_setup !== undefined) return _setup;
|
|
16
|
+
try { _setup = req('openclaw/plugin-sdk/setup'); } catch { _setup = null; }
|
|
17
|
+
return _setup;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadTools() {
|
|
21
|
+
if (_tools !== undefined) return _tools;
|
|
22
|
+
try { _tools = req('openclaw/plugin-sdk/setup-tools'); } catch { _tools = null; }
|
|
23
|
+
return _tools;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type { ChannelSetupWizard } from 'openclaw/plugin-sdk/setup';
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_ACCOUNT_ID = 'default';
|
|
29
|
+
|
|
30
|
+
export function createStandardChannelSetupStatus(
|
|
31
|
+
...args: Parameters<NonNullable<typeof _setup>['createStandardChannelSetupStatus']>
|
|
32
|
+
): ReturnType<NonNullable<typeof _setup>['createStandardChannelSetupStatus']> {
|
|
33
|
+
const mod = loadSetup();
|
|
34
|
+
if (mod) return mod.createStandardChannelSetupStatus(...args);
|
|
35
|
+
return {
|
|
36
|
+
channelLabel: args[0]?.channelLabel ?? 'QQ Bot',
|
|
37
|
+
configuredLabel: 'Configured',
|
|
38
|
+
unconfiguredLabel: 'Not configured',
|
|
39
|
+
resolveConfigured: () => false,
|
|
40
|
+
} as unknown as ReturnType<NonNullable<typeof _setup>['createStandardChannelSetupStatus']>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setSetupChannelEnabled(
|
|
44
|
+
...args: Parameters<NonNullable<typeof _setup>['setSetupChannelEnabled']>
|
|
45
|
+
): void {
|
|
46
|
+
loadSetup()?.setSetupChannelEnabled?.(...args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatDocsLink(
|
|
50
|
+
...args: Parameters<NonNullable<typeof _tools>['formatDocsLink']>
|
|
51
|
+
): ReturnType<NonNullable<typeof _tools>['formatDocsLink']> {
|
|
52
|
+
const mod = loadTools();
|
|
53
|
+
if (mod) return mod.formatDocsLink(...args);
|
|
54
|
+
return (args[1] ? `${args[1]}: ${args[0]}` : args[0]) as ReturnType<NonNullable<typeof _tools>['formatDocsLink']>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginWebhookAdapter — 将 OpenClaw Gateway HTTP 路由适配为 SDK WebhookServerAdapter。
|
|
3
|
+
*
|
|
4
|
+
* 复用 openclaw/plugin-sdk/webhook-ingress 的 target registry、rate limiting 和
|
|
5
|
+
* resolveWebhookTargetWithAuthOrRejectSync 签名匹配,对齐旧版本插件行为。
|
|
6
|
+
*
|
|
7
|
+
* 多账号同路径:通过 resolveWebhookTargetWithAuthOrRejectSync 遍历 targets 做
|
|
8
|
+
* Ed25519 签名匹配,找到正确的 target 后调用其 SDK handler 处理事件。
|
|
9
|
+
*/
|
|
10
|
+
import type { ResolvedQQBotAccount } from '../types.js';
|
|
11
|
+
import type { PluginLogger } from '../utils/plugin-logger.js';
|
|
12
|
+
import type { WebhookServerAdapter, WebhookRequestHandler } from '@tencent-connect/qqbot-nodejs';
|
|
13
|
+
import { verifyWebhookSignature } from '@tencent-connect/qqbot-nodejs/protocol';
|
|
14
|
+
|
|
15
|
+
// ── 模块级共享状态 ──
|
|
16
|
+
interface WebhookTarget {
|
|
17
|
+
path: string;
|
|
18
|
+
accountId: string;
|
|
19
|
+
appId: string;
|
|
20
|
+
clientSecret: string;
|
|
21
|
+
handler: WebhookRequestHandler;
|
|
22
|
+
}
|
|
23
|
+
const sharedTargets = new Map<string, WebhookTarget[]>();
|
|
24
|
+
const _routeUnregisters = new Map<string, () => void>();
|
|
25
|
+
|
|
26
|
+
export function createPluginWebhookAdapter(params: {
|
|
27
|
+
account: ResolvedQQBotAccount;
|
|
28
|
+
log: PluginLogger;
|
|
29
|
+
}): WebhookServerAdapter {
|
|
30
|
+
let unregistered = false;
|
|
31
|
+
let storedPath = '';
|
|
32
|
+
const accountId = params.account.accountId;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async listen(_port: number, path: string, handler: WebhookRequestHandler): Promise<void> {
|
|
36
|
+
const ingress = await import('openclaw/plugin-sdk/webhook-ingress').catch(() => null);
|
|
37
|
+
if (!ingress?.registerWebhookTargetWithPluginRoute) {
|
|
38
|
+
params.log.error('Webhook ingress not available');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const webhookPath = storedPath = path && path !== '/' ? path : (params.account.config.webhook?.path ?? '/qqbot/webhook');
|
|
43
|
+
|
|
44
|
+
// 去重:reload 时避免同一 accountId 重复注册
|
|
45
|
+
const existing = sharedTargets.get(webhookPath);
|
|
46
|
+
if (existing) {
|
|
47
|
+
const dupIdx = existing.findIndex((t) => t.accountId === accountId);
|
|
48
|
+
if (dupIdx >= 0) {
|
|
49
|
+
existing[dupIdx] = { path: webhookPath, accountId, appId: params.account.appId, clientSecret: params.account.clientSecret, handler };
|
|
50
|
+
params.log.info(`Webhook target refreshed for ${accountId} on ${webhookPath}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 注册 target(框架管理 targetsByPath + 首次触发路由注册)
|
|
56
|
+
const target: WebhookTarget = {
|
|
57
|
+
path: webhookPath,
|
|
58
|
+
accountId,
|
|
59
|
+
appId: params.account.appId,
|
|
60
|
+
clientSecret: params.account.clientSecret,
|
|
61
|
+
handler,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = ingress.registerWebhookTargetWithPluginRoute({
|
|
65
|
+
targetsByPath: sharedTargets as any,
|
|
66
|
+
target,
|
|
67
|
+
route: {
|
|
68
|
+
auth: 'plugin',
|
|
69
|
+
match: 'exact',
|
|
70
|
+
pluginId: 'openclaw-qqbot',
|
|
71
|
+
source: 'qqbot-webhook',
|
|
72
|
+
accountId,
|
|
73
|
+
replaceExisting: true,
|
|
74
|
+
log: (msg: string) => params.log.info(msg),
|
|
75
|
+
handler: createSharedHandler(webhookPath, params.log),
|
|
76
|
+
},
|
|
77
|
+
onLastPathTargetRemoved: () => {
|
|
78
|
+
params.log.info(`Last webhook target removed from ${webhookPath}`);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
_routeUnregisters.set(webhookPath, result.unregister);
|
|
82
|
+
|
|
83
|
+
params.log.info(`Webhook target added on ${webhookPath} (${sharedTargets.get(webhookPath)!.length} account(s))`);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
close(): void {
|
|
87
|
+
if (unregistered) return;
|
|
88
|
+
unregistered = true;
|
|
89
|
+
if (storedPath) {
|
|
90
|
+
_routeUnregisters.get(storedPath)?.();
|
|
91
|
+
_routeUnregisters.delete(storedPath);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createSharedHandler(path: string, log: PluginLogger) {
|
|
98
|
+
return async (req: any, res: any) => {
|
|
99
|
+
try {
|
|
100
|
+
const ingress = await import('openclaw/plugin-sdk/webhook-ingress').catch(() => null);
|
|
101
|
+
|
|
102
|
+
if (!ingress?.withResolvedWebhookRequestPipeline) {
|
|
103
|
+
await handleSimple(req, res, path, log);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await ingress.withResolvedWebhookRequestPipeline({
|
|
108
|
+
req,
|
|
109
|
+
res,
|
|
110
|
+
targetsByPath: sharedTargets,
|
|
111
|
+
rateLimiter: ingress.createFixedWindowRateLimiter?.({
|
|
112
|
+
windowMs: 60_000, maxRequests: 600, maxTrackedKeys: 4096,
|
|
113
|
+
}),
|
|
114
|
+
inFlightLimiter: ingress.createWebhookInFlightLimiter?.({
|
|
115
|
+
maxInFlightPerKey: 8, maxTrackedKeys: 4096,
|
|
116
|
+
}),
|
|
117
|
+
requireJsonContentType: true,
|
|
118
|
+
handle: async ({ targets }) => {
|
|
119
|
+
const bodyResult = await ingress.readWebhookBodyOrReject({
|
|
120
|
+
req, res, maxBytes: 1_048_576, timeoutMs: 30_000,
|
|
121
|
+
});
|
|
122
|
+
if (!bodyResult.ok) {
|
|
123
|
+
log.warn?.(`[webhook] body rejected`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rawBody = Buffer.from(bodyResult.value, 'utf-8');
|
|
128
|
+
|
|
129
|
+
let payload: any;
|
|
130
|
+
try { payload = JSON.parse(bodyResult.value); } catch (err) {
|
|
131
|
+
log.warn?.(`[webhook] invalid json: ${(err as Error).message}`);
|
|
132
|
+
res.statusCode = 400; res.end(JSON.stringify({ error: 'invalid json' })); return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// op:13 → 用第一个 target 的 SDK handler(无需签名)
|
|
136
|
+
if (payload.op === 13) {
|
|
137
|
+
const t = targets[0];
|
|
138
|
+
if (!t) {
|
|
139
|
+
log.warn?.(`[webhook] op:13 no target on ${path}`);
|
|
140
|
+
res.statusCode = 500; res.end(JSON.stringify({ error: 'no target' })); return;
|
|
141
|
+
}
|
|
142
|
+
const h = resolveTargetHandler(path, t.accountId);
|
|
143
|
+
if (!h) {
|
|
144
|
+
log.warn?.(`[webhook] op:13 no handler for ${t.accountId}`);
|
|
145
|
+
res.statusCode = 500; res.end(JSON.stringify({ error: 'no handler' })); return;
|
|
146
|
+
}
|
|
147
|
+
await delegateToHandler(h, req, res, rawBody);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// op:0 → 签名匹配 target → SDK handler 处理事件
|
|
152
|
+
const timestamp = getHeader(req, 'x-signature-timestamp') ?? '';
|
|
153
|
+
const signature = getHeader(req, 'x-signature-ed25519') ?? '';
|
|
154
|
+
if (!timestamp || !signature) {
|
|
155
|
+
log.warn?.(`[webhook] missing signature headers on ${req.url}`);
|
|
156
|
+
res.statusCode = 401; res.end(JSON.stringify({ error: 'missing signature' })); return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const matched = ingress.resolveWebhookTargetWithAuthOrRejectSync({
|
|
160
|
+
targets, res,
|
|
161
|
+
isMatch: (t: any) => verifyWebhookSignature({
|
|
162
|
+
body: rawBody, timestamp, signature, botSecret: t.clientSecret as string,
|
|
163
|
+
}),
|
|
164
|
+
unauthorizedStatusCode: 401,
|
|
165
|
+
unauthorizedMessage: JSON.stringify({ error: 'invalid signature' }),
|
|
166
|
+
});
|
|
167
|
+
if (!matched) {
|
|
168
|
+
log.warn?.(`[webhook] signature mismatch on ${path} (${targets.length} target(s))`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const h = resolveTargetHandler(path, matched.accountId);
|
|
173
|
+
if (!h) {
|
|
174
|
+
log.error?.(`[webhook] no handler for matched target ${matched.accountId}`);
|
|
175
|
+
res.statusCode = 500; res.end(JSON.stringify({ error: 'no handler' })); return;
|
|
176
|
+
}
|
|
177
|
+
await delegateToHandler(h, req, res, rawBody);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
log.error(`Webhook handler error: ${(err as Error).message}`);
|
|
182
|
+
if (!res.headersSent) {
|
|
183
|
+
res.statusCode = 500;
|
|
184
|
+
res.setHeader('Content-Type', 'application/json');
|
|
185
|
+
res.end(JSON.stringify({ error: 'internal error' }));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveTargetHandler(path: string, accountId: string): WebhookRequestHandler | undefined {
|
|
192
|
+
return sharedTargets.get(path)?.find((t) => t.accountId === accountId)?.handler;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function delegateToHandler(handler: WebhookRequestHandler, req: any, res: any, rawBody: Buffer) {
|
|
196
|
+
const headers: Record<string, string | string[]> = {};
|
|
197
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
198
|
+
headers[k.toLowerCase()] = v as string | string[];
|
|
199
|
+
}
|
|
200
|
+
const resp = await handler({ body: rawBody, headers });
|
|
201
|
+
res.statusCode = resp.status;
|
|
202
|
+
if (resp.headers) {
|
|
203
|
+
for (const [k, v] of Object.entries(resp.headers)) res.setHeader(k, v as string);
|
|
204
|
+
}
|
|
205
|
+
res.end(resp.body);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function handleSimple(req: any, res: any, path: string, log: PluginLogger) {
|
|
209
|
+
try {
|
|
210
|
+
const ct = String(req.headers['content-type'] ?? '');
|
|
211
|
+
if (!ct.includes('application/json')) { res.statusCode = 400; res.end(JSON.stringify({ error: 'unsupported content type' })); return; }
|
|
212
|
+
|
|
213
|
+
const chunks: Buffer[] = [];
|
|
214
|
+
let total = 0;
|
|
215
|
+
for await (const chunk of req) {
|
|
216
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
217
|
+
total += buf.length;
|
|
218
|
+
if (total > 1_048_576) { res.statusCode = 413; res.end(JSON.stringify({ error: 'too large' })); return; }
|
|
219
|
+
chunks.push(buf);
|
|
220
|
+
}
|
|
221
|
+
const rawBody = Buffer.concat(chunks);
|
|
222
|
+
const entries = sharedTargets.get(path) ?? [];
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
const resp = await entry.handler({ body: rawBody, headers: mapHeaders(req) });
|
|
225
|
+
if (resp.status < 400) {
|
|
226
|
+
res.statusCode = resp.status;
|
|
227
|
+
if (resp.headers) for (const [k, v] of Object.entries(resp.headers)) res.setHeader(k, v as string);
|
|
228
|
+
res.end(resp.body);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
res.statusCode = 401; res.end(JSON.stringify({ error: 'invalid signature' }));
|
|
233
|
+
} catch (err) {
|
|
234
|
+
log.error(`Webhook simple handler error: ${(err as Error).message}`);
|
|
235
|
+
if (!res.headersSent) { res.statusCode = 500; res.end(JSON.stringify({ error: 'internal error' })); }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function mapHeaders(req: any): Record<string, string | string[]> {
|
|
240
|
+
const h: Record<string, string | string[]> = {};
|
|
241
|
+
for (const [k, v] of Object.entries(req.headers ?? {})) h[k.toLowerCase()] = v as string | string[];
|
|
242
|
+
return h;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getHeader(req: any, key: string): string | undefined {
|
|
246
|
+
const val = req.headers[key];
|
|
247
|
+
return Array.isArray(val) ? val[0] : val;
|
|
248
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent workspace 目录解析(动态加载 plugin-sdk/health)
|
|
3
|
+
*
|
|
4
|
+
* 新版本 openclaw 有 openclaw/plugin-sdk/health 导出,
|
|
5
|
+
* 旧版本不可用时 fallback 到 QQ Bot media 目录。
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { getQQBotMediaDir } from '../utils/platform.js';
|
|
9
|
+
|
|
10
|
+
const req = createRequire(__filename);
|
|
11
|
+
|
|
12
|
+
let health: {
|
|
13
|
+
resolveAgentWorkspaceDir: (cfg: any, agentId: string) => string;
|
|
14
|
+
resolveDefaultAgentId: (cfg: any) => string;
|
|
15
|
+
} | null = null;
|
|
16
|
+
|
|
17
|
+
export function resolveAgentWorkspace(cfg: any, agentId?: string): string {
|
|
18
|
+
if (!health) {
|
|
19
|
+
try {
|
|
20
|
+
health = req('openclaw/plugin-sdk/health') as typeof health;
|
|
21
|
+
} catch {
|
|
22
|
+
health = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (health) {
|
|
26
|
+
return health.resolveAgentWorkspaceDir(cfg, agentId ?? health.resolveDefaultAgentId(cfg));
|
|
27
|
+
}
|
|
28
|
+
return getQQBotMediaDir();
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot 实例访问器
|
|
3
|
+
*
|
|
4
|
+
* 提供统一的方式获取当前活跃的 QQBot SDK 实例。
|
|
5
|
+
* 替代原 api.ts 的所有功能 — 消费者通过 getBotForAccount() 获取 bot,
|
|
6
|
+
* 然后使用 bot.api / bot.send() 等 SDK 原生能力。
|
|
7
|
+
*/
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import type { QQBot } from '@tencent-connect/qqbot-nodejs';
|
|
10
|
+
import { getGateway } from './outbound/outbound-service.js';
|
|
11
|
+
import { getPackageVersion } from './utils/pkg-version.js';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const PLUGIN_VERSION = getPackageVersion();
|
|
15
|
+
let _openclawVersion = 'unknown';
|
|
16
|
+
|
|
17
|
+
export function setOpenClawVersion(version: string): void {
|
|
18
|
+
if (version) _openclawVersion = version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getOpenClawVersion(): string {
|
|
22
|
+
return _openclawVersion;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildUserAgent(suffix?: string): string {
|
|
26
|
+
const base = `QQBotPlugin/${PLUGIN_VERSION} (Node/${process.versions.node}; ${os.platform()}; OpenClaw/${_openclawVersion})`;
|
|
27
|
+
return suffix ? `${base} ${suffix}` : base;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Bot 实例获取 ──
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 获取指定账户的 QQBot SDK 实例。
|
|
34
|
+
*
|
|
35
|
+
* @throws 如果该账户的 gateway 尚未启动
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const bot = getBotForAccount(accountId);
|
|
40
|
+
* await bot.send({ target, content: 'hello' });
|
|
41
|
+
* const guilds = await bot.api.get('/users/@me/guilds');
|
|
42
|
+
* const token = await bot.api.getToken();
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function getBotForAccount(accountId: string): QQBot {
|
|
46
|
+
const gw = getGateway(accountId);
|
|
47
|
+
if (!gw) {
|
|
48
|
+
throw new Error(`[qqbot] Bot "${accountId}" not running — gateway not started`);
|
|
49
|
+
}
|
|
50
|
+
return gw.bot;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 尝试获取指定账户的 QQBot SDK 实例(不抛异常)。
|
|
55
|
+
* 返回 null 表示 gateway 尚未启动。
|
|
56
|
+
*/
|
|
57
|
+
export function tryGetBotForAccount(accountId: string): QQBot | null {
|
|
58
|
+
const gw = getGateway(accountId);
|
|
59
|
+
return gw?.bot ?? null;
|
|
60
|
+
}
|