@jerryliang122/openclaw-qqbot 1.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/LICENSE +22 -0
- package/README.md +967 -0
- package/README.zh.md +790 -0
- package/dist/index.cjs +18072 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1222 -0
- package/index.ts +92 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +69 -0
- package/preload.cjs +19 -0
- package/scripts/link-sdk-core.cjs +268 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/skills/qqbot-channel/SKILL.md +285 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-remind/SKILL.md +159 -0
- package/skills/qqbot-upgrade/SKILL.md +56 -0
- package/src/adapter/contract.ts +63 -0
- package/src/adapter/lint.ts +144 -0
- package/src/adapter/media.ts +40 -0
- package/src/adapter/pairing.ts +95 -0
- package/src/adapter/resolve.ts +255 -0
- package/src/adapter/setup.ts +13 -0
- package/src/adapter/webhook.ts +248 -0
- package/src/adapter/workspace.ts +21 -0
- package/src/agent-prompt-adapter.ts +26 -0
- package/src/bot-instance.ts +60 -0
- package/src/channel.ts +230 -0
- 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-group-info.ts +48 -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 +50 -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 +51 -0
- package/src/config.ts +403 -0
- package/src/constants.ts +6 -0
- package/src/dispatch/body-assembler.ts +308 -0
- package/src/dispatch/ctx-builder.ts +127 -0
- package/src/dispatch/dispatch.ts +667 -0
- package/src/dispatch/envelope-builder.ts +112 -0
- package/src/dispatch/index.ts +2 -0
- package/src/features/approval-capability.ts +302 -0
- package/src/features/approval-helpers.ts +271 -0
- package/src/features/approval-utils.ts +21 -0
- package/src/features/command-panel.ts +301 -0
- package/src/features/credential-backup.ts +74 -0
- package/src/features/group-mode-store.ts +79 -0
- package/src/features/history-store.ts +75 -0
- package/src/features/msgid-cache.ts +55 -0
- package/src/features/outbound-echo-store.ts +46 -0
- package/src/features/proactive-budget.ts +57 -0
- package/src/features/proactive.ts +549 -0
- package/src/features/question-helpers.ts +771 -0
- package/src/features/quota-manager.ts +173 -0
- package/src/features/ref-index-store.ts +289 -0
- package/src/features/secret-input-store.ts +118 -0
- package/src/features/secret-store-cli.ts +324 -0
- package/src/features/typing-refresh.ts +51 -0
- package/src/features/update-checker.ts +166 -0
- package/src/gateway/event-handlers.ts +456 -0
- package/src/gateway/index.ts +3 -0
- package/src/gateway/lifecycle.ts +236 -0
- package/src/gateway/middleware-setup.ts +173 -0
- package/src/gateway/qqbot-gateway.ts +458 -0
- package/src/gateway-adapter.ts +44 -0
- package/src/heartbeat-adapter.ts +57 -0
- package/src/message-adapter.ts +40 -0
- package/src/messaging-adapter.ts +78 -0
- package/src/middleware/access-control.ts +125 -0
- package/src/middleware/attachment.ts +373 -0
- package/src/middleware/inbound-guard.ts +102 -0
- package/src/middleware/policy-injector.ts +71 -0
- package/src/middleware/secret-capture.ts +161 -0
- package/src/middleware/typing.ts +110 -0
- package/src/openclaw-plugin-sdk.d.ts +543 -0
- package/src/outbound/chunker.ts +80 -0
- package/src/outbound/debounce.ts +102 -0
- package/src/outbound/deliver-pipeline.ts +235 -0
- package/src/outbound/index.ts +3 -0
- package/src/outbound/local-file-router.ts +145 -0
- package/src/outbound/media-send.ts +408 -0
- package/src/outbound/outbound-service.ts +298 -0
- package/src/outbound/reply-limiter.ts +139 -0
- package/src/outbound/sanitize.ts +32 -0
- package/src/outbound/streaming-controller.ts +332 -0
- package/src/outbound/target.ts +109 -0
- package/src/outbound-adapter.ts +323 -0
- package/src/plugin-base.ts +42 -0
- package/src/request-context.ts +50 -0
- package/src/runtime.ts +42 -0
- 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/status-adapter.ts +56 -0
- package/src/tools/platform.ts +149 -0
- package/src/tools/remind.ts +308 -0
- package/src/tools/secret-input.ts +185 -0
- package/src/types-augment.d.ts +54 -0
- package/src/types-plugin.ts +82 -0
- package/src/types.ts +620 -0
- package/src/typing-lifecycle.ts +182 -0
- package/src/utils/mention.ts +52 -0
- package/src/utils/pkg-version.ts +23 -0
- package/src/utils/platform.ts +459 -0
- package/src/utils/plugin-logger.ts +104 -0
- package/src/utils/ssrf-guard.ts +132 -0
- package/src/utils/stt.ts +150 -0
- package/src/utils/voice-text.ts +61 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +64 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
import * as openclaw_plugin_sdk_core from 'openclaw/plugin-sdk/core';
|
|
2
|
+
import { PluginRuntime, OpenClawConfig, GroupPolicy as GroupPolicy$1, OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
3
|
+
import * as openclaw_plugin_sdk_channel_core from 'openclaw/plugin-sdk/channel-core';
|
|
4
|
+
import { QQBot, ReplyTarget, MessageResponse, MediaFileType, StreamSession, MiddlewareContext, QQBotInboundMessage, RefIndexStore, RefEntry, Middleware } from '@tencent-connect/qqbot-nodejs';
|
|
5
|
+
|
|
6
|
+
/** 普通文本消息 */
|
|
7
|
+
declare const MSG_TYPE_TEXT = 0;
|
|
8
|
+
/** 引用(回复)消息 */
|
|
9
|
+
declare const MSG_TYPE_QUOTE = 103;
|
|
10
|
+
/**
|
|
11
|
+
* QQ Bot 配置类型
|
|
12
|
+
*/
|
|
13
|
+
interface QQBotConfig {
|
|
14
|
+
appId: string;
|
|
15
|
+
clientSecret?: string;
|
|
16
|
+
clientSecretFile?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 解析后的 QQ Bot 账户
|
|
20
|
+
*/
|
|
21
|
+
interface ResolvedQQBotAccount {
|
|
22
|
+
accountId: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
appId: string;
|
|
26
|
+
clientSecret: string;
|
|
27
|
+
secretSource: "config" | "file" | "env" | "none";
|
|
28
|
+
/** 系统提示词 */
|
|
29
|
+
systemPrompt?: string;
|
|
30
|
+
/** 是否支持 markdown 消息(默认 true) */
|
|
31
|
+
markdownSupport: boolean;
|
|
32
|
+
/** 是否自动同步 openclaw essential 原生指令到 QQ Bot 指令面板(默认 true) */
|
|
33
|
+
commandPanelNative: boolean;
|
|
34
|
+
/** User-Agent 尾部追加内容 */
|
|
35
|
+
userAgentSuffix: string;
|
|
36
|
+
config: QQBotAccountConfig;
|
|
37
|
+
}
|
|
38
|
+
/** 群消息策略:open=全响应 | allowlist=白名单 | disabled=不响应 */
|
|
39
|
+
type GroupPolicy = "open" | "allowlist" | "disabled";
|
|
40
|
+
/** 工具策略:full=全部 | restricted=限制敏感工具 | none=禁止 */
|
|
41
|
+
type ToolPolicy = "full" | "restricted" | "none";
|
|
42
|
+
/** 群消息合并配置 */
|
|
43
|
+
interface GroupCoalesceConfig {
|
|
44
|
+
/**
|
|
45
|
+
* 是否启用消息合并(默认 true):
|
|
46
|
+
* - true:框架 collect 合并批处理
|
|
47
|
+
* - false:followup 排队不合并
|
|
48
|
+
* 排队/合并完全由 OpenClaw 框架的 followup 队列承担。
|
|
49
|
+
*/
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** 指令面板配置 */
|
|
53
|
+
interface CommandsConfig {
|
|
54
|
+
/**
|
|
55
|
+
* 是否把 openclaw essential 原生指令自动注册到 QQ Bot 指令面板(默认 true)。
|
|
56
|
+
* 面板项点击后指令文本填入输入框,以普通文本进入框架,走文本斜杠指令路由执行。
|
|
57
|
+
*/
|
|
58
|
+
native?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** 单个群的配置 */
|
|
61
|
+
interface GroupConfig {
|
|
62
|
+
/** 是否需要 @机器人才响应(默认 true) */
|
|
63
|
+
requireMention?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* 是否忽略 @了其他用户但没有 @机器人的消息(默认 false)。
|
|
66
|
+
* 开启后,消息中 @了其他人但未 @bot 时直接丢弃(不记录历史、不触发 AI)。
|
|
67
|
+
*/
|
|
68
|
+
ignoreOtherMentions?: boolean;
|
|
69
|
+
/** 群聊中 AI 可使用的工具范围(默认 restricted) */
|
|
70
|
+
toolPolicy?: ToolPolicy;
|
|
71
|
+
/** 群名称 */
|
|
72
|
+
name?: string;
|
|
73
|
+
/** 群消息行为 PE(未配置时使用内置默认值) */
|
|
74
|
+
prompt?: string;
|
|
75
|
+
/** 群历史消息缓存条数(0 禁用,默认 20) */
|
|
76
|
+
historyLimit?: number;
|
|
77
|
+
/**
|
|
78
|
+
* 群历史清理时机(默认 clear):
|
|
79
|
+
* - clear:每次回复后整清(旧语义,"自上次回复以来的窗口")
|
|
80
|
+
* - rolling:回复后裁剪到最后一条 bot 出站之后(bot 发言也计入历史,
|
|
81
|
+
* 对齐 telegram selectAfterLastSelf 的滚动窗口语义)
|
|
82
|
+
*/
|
|
83
|
+
historyMode?: 'clear' | 'rolling';
|
|
84
|
+
/**
|
|
85
|
+
* 未被 @ 的群消息入站策略(默认 user_request,仅全量模式群有实际意义):
|
|
86
|
+
* - user_request:维持现状——mentionGate 拦截未 @ 消息(只进历史,不上报)
|
|
87
|
+
* - room_event:门控放行全部消息,未被 @/称呼/引用的作为 room_event 被动
|
|
88
|
+
* 房间事件进框架(AI 只读上下文,不自动回复,想发言走主动 message 工具;
|
|
89
|
+
* 框架自动压制 typing/流式/steer)。成本提示:每条消息一次推理 pass。
|
|
90
|
+
*/
|
|
91
|
+
unmentionedInbound?: 'user_request' | 'room_event';
|
|
92
|
+
/** 群消息合并配置(覆盖账号级配置) */
|
|
93
|
+
coalesce?: GroupCoalesceConfig;
|
|
94
|
+
}
|
|
95
|
+
/** 限流单层配置(滑动窗口) */
|
|
96
|
+
interface RateLimitTierConfig {
|
|
97
|
+
/** 窗口内最大消息数 */
|
|
98
|
+
max: number;
|
|
99
|
+
/** 窗口时长(毫秒) */
|
|
100
|
+
windowMs: number;
|
|
101
|
+
}
|
|
102
|
+
/** 三层限流配置(sender / group / global) */
|
|
103
|
+
interface RateLimitConfig {
|
|
104
|
+
/** 是否启用(默认 true;保守默认阈值,正常使用不会触发) */
|
|
105
|
+
enabled?: boolean;
|
|
106
|
+
/** 单发送者限流(默认 20 条/分钟) */
|
|
107
|
+
perSender?: RateLimitTierConfig;
|
|
108
|
+
/** 单群限流(默认 60 条/分钟,c2c 按 sender 归组) */
|
|
109
|
+
perGroup?: RateLimitTierConfig;
|
|
110
|
+
/** 全局限流(默认 300 条/分钟) */
|
|
111
|
+
global?: RateLimitTierConfig;
|
|
112
|
+
}
|
|
113
|
+
/** 消息接收传输方式 */
|
|
114
|
+
type TransportMode = "websocket" | "webhook";
|
|
115
|
+
/** Webhook 传输配置 */
|
|
116
|
+
interface WebhookTransportConfig {
|
|
117
|
+
/** 监听路径(默认 /qqbot/webhook) */
|
|
118
|
+
path?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* QQ Bot 账户配置
|
|
122
|
+
*/
|
|
123
|
+
interface QQBotAccountConfig {
|
|
124
|
+
enabled?: boolean;
|
|
125
|
+
name?: string;
|
|
126
|
+
appId?: string;
|
|
127
|
+
clientSecret?: string;
|
|
128
|
+
clientSecretFile?: string;
|
|
129
|
+
dmPolicy?: "open" | "pairing" | "allowlist" | "disabled";
|
|
130
|
+
allowFrom?: string[];
|
|
131
|
+
/** 消息接收传输方式:websocket(默认)| webhook */
|
|
132
|
+
transport?: TransportMode;
|
|
133
|
+
/** webhook 传输配置(transport="webhook" 时生效) */
|
|
134
|
+
webhook?: WebhookTransportConfig;
|
|
135
|
+
/** 群消息策略(默认 allowlist) */
|
|
136
|
+
groupPolicy?: GroupPolicy;
|
|
137
|
+
/** 群白名单(groupPolicy 为 allowlist 时生效) */
|
|
138
|
+
groupAllowFrom?: string[];
|
|
139
|
+
/** 群配置映射(按 groupOpenid 索引,"*" 为默认) */
|
|
140
|
+
groups?: Record<string, GroupConfig>;
|
|
141
|
+
/** 三层限流(sender/group/global,默认启用保守阈值) */
|
|
142
|
+
rateLimit?: RateLimitConfig;
|
|
143
|
+
/** 系统提示词,会添加在用户消息前面 */
|
|
144
|
+
systemPrompt?: string;
|
|
145
|
+
/** 是否支持 markdown 消息(默认 true,设为 false 可禁用) */
|
|
146
|
+
markdownSupport?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* 音频格式策略配置
|
|
149
|
+
* 统一管理入站(STT)和出站(上传)的音频格式转换行为
|
|
150
|
+
*/
|
|
151
|
+
audioFormatPolicy?: AudioFormatPolicy;
|
|
152
|
+
/**
|
|
153
|
+
* 是否启用公网 URL 直传 QQ 平台(默认 true)
|
|
154
|
+
* 启用时:公网 URL 先直传给 QQ 开放平台的富媒体 API,平台自行拉取;失败后自动 fallback 到插件下载再 Base64 上传
|
|
155
|
+
* 禁用时:公网 URL 始终由插件先下载到本地,再以 Base64 上传(适用于 QQ 平台无法访问目标 URL 的场景)
|
|
156
|
+
*/
|
|
157
|
+
urlDirectUpload?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* /bot-upgrade 指令返回的升级指引网址
|
|
160
|
+
* 默认: 本仓库 CHANGELOG(含各版本升级说明)
|
|
161
|
+
*/
|
|
162
|
+
upgradeUrl?: string;
|
|
163
|
+
/**
|
|
164
|
+
* 群消息是否默认需要 @机器人才响应(默认 true)
|
|
165
|
+
* 优先级低于 groups.{groupId}.requireMention 和 groups."*".requireMention
|
|
166
|
+
* 设为 false 时,所有群默认无需 @ 即触发回复(仍可被群级配置覆盖)
|
|
167
|
+
*/
|
|
168
|
+
defaultRequireMention?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* 群消息合并配置(账号级)
|
|
171
|
+
* 启用后,群聊中快速发送的多条消息会被合并处理,而不是取消之前的任务
|
|
172
|
+
* 与私聊的"插嘴"行为相反,群聊中所有消息都应该被处理
|
|
173
|
+
*/
|
|
174
|
+
groupCoalesce?: GroupCoalesceConfig;
|
|
175
|
+
/**
|
|
176
|
+
* 出站消息合并回复(debounce)配置
|
|
177
|
+
* 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
|
|
178
|
+
*/
|
|
179
|
+
deliverDebounce?: DeliverDebounceConfig;
|
|
180
|
+
/**
|
|
181
|
+
* "正在输入"指示器配置(仅 C2C 私聊生效)
|
|
182
|
+
*/
|
|
183
|
+
typing?: TypingIndicatorConfig;
|
|
184
|
+
/**
|
|
185
|
+
* 指令面板配置(openclaw 原生指令 → QQ Bot 指令面板自动注册)
|
|
186
|
+
*/
|
|
187
|
+
commands?: CommandsConfig;
|
|
188
|
+
/**
|
|
189
|
+
* 是否启用流式消息(默认 false)
|
|
190
|
+
* 启用后,AI 的回复会以流式形式逐步显示在 QQ 聊天中,
|
|
191
|
+
* 用户可以看到文字逐字出现的打字机效果。
|
|
192
|
+
*
|
|
193
|
+
* 兼容布尔值和对象格式,对齐框架 schema:
|
|
194
|
+
* - true / false 旧版布尔格式(自动转换为对象)
|
|
195
|
+
* - { mode: "partial" } 开启(对齐 StreamingMode.partial)
|
|
196
|
+
* - { mode: "off" } 关闭
|
|
197
|
+
*
|
|
198
|
+
* 注意:仅 C2C(私聊)支持流式消息 API。
|
|
199
|
+
*
|
|
200
|
+
* sendMode 控制文本下发通道(仅 mode="partial" 时生效):
|
|
201
|
+
* - "stream" QQ 流式打印机(默认):同一条消息内容不断变长,打字机效果
|
|
202
|
+
* - "static" 流结束时用一条普通 sendText 发送完整文本:无打字机,
|
|
203
|
+
* partial 接收逻辑(状态机/串行/去重)保持不变,仅替换下发通道。
|
|
204
|
+
* 适合不想要打字机效果、只想收到一条完整回复的场景。
|
|
205
|
+
*/
|
|
206
|
+
streaming?: {
|
|
207
|
+
mode: 'partial' | 'off';
|
|
208
|
+
sendMode?: 'stream' | 'static';
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* STT (语音转文字) 配置
|
|
212
|
+
* 配置后,收到语音消息时会自动调用 STT 服务转录为文字
|
|
213
|
+
*/
|
|
214
|
+
stt?: STTChannelConfig;
|
|
215
|
+
/**
|
|
216
|
+
* User-Agent 尾部追加内容(用于私有化部署标识等场景)
|
|
217
|
+
* 追加在 `QQBotPlugin/{version} (Node/{nodeVersion}; {os}; OpenClaw/{version})` 之后
|
|
218
|
+
*/
|
|
219
|
+
userAgentSuffix?: string;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 出站消息合并回复配置
|
|
223
|
+
*/
|
|
224
|
+
interface DeliverDebounceConfig {
|
|
225
|
+
/**
|
|
226
|
+
* 是否启用合并回复(默认 true)
|
|
227
|
+
*/
|
|
228
|
+
enabled?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* 合并窗口时长(毫秒),在此时间内的连续 deliver 会被合并
|
|
231
|
+
* 默认 1500ms
|
|
232
|
+
*/
|
|
233
|
+
windowMs?: number;
|
|
234
|
+
/**
|
|
235
|
+
* 最大等待时长(毫秒),从第一条 deliver 开始计算,超过此时间强制发送
|
|
236
|
+
* 防止持续有新 deliver 导致一直不发送
|
|
237
|
+
* 默认 8000ms
|
|
238
|
+
*/
|
|
239
|
+
maxWaitMs?: number;
|
|
240
|
+
/**
|
|
241
|
+
* 合并文本之间的分隔符
|
|
242
|
+
* 默认 "\n\n---\n\n"
|
|
243
|
+
*/
|
|
244
|
+
separator?: string;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* "正在输入"指示器配置
|
|
248
|
+
*
|
|
249
|
+
* QQ 客户端行为:退出聊天界面再进入后,指示器会消失,只有收到新的
|
|
250
|
+
* input_notify 推送才会重新显示,因此处理期间需要周期性续期。
|
|
251
|
+
*
|
|
252
|
+
* 配额说明:typing 通知与回复消息共享同一 msg_id 的被动回复配额
|
|
253
|
+
* (QQ 开放平台同一条消息被动回复上限约 5 条)。被动配额耗尽后,
|
|
254
|
+
* typing 与回复消息一样自动降级为主动发送(不带 msg_id),续期不中断。
|
|
255
|
+
*
|
|
256
|
+
* 中间消息:机器人发出消息(如思维链中间输出)后,QQ 客户端会终止
|
|
257
|
+
* 指示器显示。若框架任务仍在进行,插件会在消息发出 5 秒后补发一次
|
|
258
|
+
* 续期;若是最终回复(任务完成),则不再补发。
|
|
259
|
+
*/
|
|
260
|
+
interface TypingIndicatorConfig {
|
|
261
|
+
/**
|
|
262
|
+
* 是否启用指示器(默认 true)
|
|
263
|
+
*/
|
|
264
|
+
enabled?: boolean;
|
|
265
|
+
/**
|
|
266
|
+
* 续期间隔(毫秒),默认 20000
|
|
267
|
+
* 受 QPS 限制,低于 20000 会被钳制到 20000
|
|
268
|
+
*/
|
|
269
|
+
intervalMs?: number;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 音频格式策略:控制哪些格式可跳过转换
|
|
273
|
+
*/
|
|
274
|
+
interface AudioFormatPolicy {
|
|
275
|
+
/**
|
|
276
|
+
* STT 模型直接支持的音频格式(入站:跳过 SILK→WAV 转换)
|
|
277
|
+
* 如果 STT 服务支持直接处理某些格式(如 silk/amr),可将其加入此列表
|
|
278
|
+
* 例如: [".silk", ".amr", ".wav", ".mp3", ".ogg"]
|
|
279
|
+
* 默认为空(所有语音都先转换为 WAV 再送 STT)
|
|
280
|
+
*/
|
|
281
|
+
sttDirectFormats?: string[];
|
|
282
|
+
/**
|
|
283
|
+
* QQ 平台支持直传的音频格式(出站:跳过→SILK 转换)
|
|
284
|
+
* 默认为 [".wav", ".mp3", ".silk"](QQ Bot API 原生支持的三种格式)
|
|
285
|
+
* 仅当需要覆盖默认值时才配置此项
|
|
286
|
+
*/
|
|
287
|
+
uploadDirectFormats?: string[];
|
|
288
|
+
/**
|
|
289
|
+
* 是否启用语音转码(默认 true)
|
|
290
|
+
* 设为 false 可在环境无 ffmpeg 时跳过转码,直接以文件形式发送
|
|
291
|
+
* 当禁用时,非原生格式的音频会 fallback 到 sendDocument(文件发送)
|
|
292
|
+
*/
|
|
293
|
+
transcodeEnabled?: boolean;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* STT (语音转文字) 配置
|
|
297
|
+
*/
|
|
298
|
+
interface STTChannelConfig {
|
|
299
|
+
/** 是否启用 STT(默认 true,配置了 baseUrl+apiKey 即自动启用) */
|
|
300
|
+
enabled?: boolean;
|
|
301
|
+
/** STT 服务提供商 ID(对应 models.providers 中的 key,默认 "openai") */
|
|
302
|
+
provider?: string;
|
|
303
|
+
/** STT API 地址(如 https://api.openai.com/v1) */
|
|
304
|
+
baseUrl?: string;
|
|
305
|
+
/** STT API 密钥 */
|
|
306
|
+
apiKey?: string;
|
|
307
|
+
/** STT 模型名称(默认 "whisper-1") */
|
|
308
|
+
model?: string;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 富媒体附件
|
|
312
|
+
*/
|
|
313
|
+
interface MessageAttachment {
|
|
314
|
+
content_type: string;
|
|
315
|
+
filename?: string;
|
|
316
|
+
height?: number;
|
|
317
|
+
width?: number;
|
|
318
|
+
size?: number;
|
|
319
|
+
url: string;
|
|
320
|
+
voice_wav_url?: string;
|
|
321
|
+
asr_refer_text?: string;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* C2C 消息事件
|
|
325
|
+
*/
|
|
326
|
+
interface C2CMessageEvent {
|
|
327
|
+
author: {
|
|
328
|
+
id: string;
|
|
329
|
+
union_openid: string;
|
|
330
|
+
user_openid: string;
|
|
331
|
+
};
|
|
332
|
+
content: string;
|
|
333
|
+
id: string;
|
|
334
|
+
timestamp: string;
|
|
335
|
+
message_scene?: {
|
|
336
|
+
source: string;
|
|
337
|
+
/** ext 数组,可能包含 ref_msg_idx=REFIDX_xxx(引用的消息)和 msg_idx=REFIDX_xxx(自身索引) */
|
|
338
|
+
ext?: string[];
|
|
339
|
+
};
|
|
340
|
+
attachments?: MessageAttachment[];
|
|
341
|
+
/** 消息类型,参见 MSG_TYPE_* */
|
|
342
|
+
message_type?: number;
|
|
343
|
+
/** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
|
|
344
|
+
msg_elements?: MsgElement[];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 频道 AT 消息事件
|
|
348
|
+
*/
|
|
349
|
+
interface GuildMessageEvent {
|
|
350
|
+
id: string;
|
|
351
|
+
channel_id: string;
|
|
352
|
+
guild_id: string;
|
|
353
|
+
content: string;
|
|
354
|
+
timestamp: string;
|
|
355
|
+
author: {
|
|
356
|
+
id: string;
|
|
357
|
+
username?: string;
|
|
358
|
+
bot?: boolean;
|
|
359
|
+
};
|
|
360
|
+
member?: {
|
|
361
|
+
nick?: string;
|
|
362
|
+
joined_at?: string;
|
|
363
|
+
};
|
|
364
|
+
attachments?: MessageAttachment[];
|
|
365
|
+
}
|
|
366
|
+
/** 消息元素结点,引用消息时 msg_elements[0] 为被引用的原始消息 */
|
|
367
|
+
interface MsgElement {
|
|
368
|
+
/** 消息索引标识 */
|
|
369
|
+
msg_idx?: string;
|
|
370
|
+
/** 消息类型,参见 MSG_TYPE_* 常量 */
|
|
371
|
+
message_type?: number;
|
|
372
|
+
/** 文本内容 */
|
|
373
|
+
content?: string;
|
|
374
|
+
/** 附件列表 */
|
|
375
|
+
attachments?: MessageAttachment[];
|
|
376
|
+
/** 嵌套消息元素(引用消息场景下可能存在) */
|
|
377
|
+
msg_elements?: MsgElement[];
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* 群聊 AT 消息事件
|
|
381
|
+
*/
|
|
382
|
+
interface GroupMessageEvent {
|
|
383
|
+
author: {
|
|
384
|
+
id: string;
|
|
385
|
+
member_openid: string;
|
|
386
|
+
username?: string;
|
|
387
|
+
bot?: boolean;
|
|
388
|
+
};
|
|
389
|
+
content: string;
|
|
390
|
+
id: string;
|
|
391
|
+
timestamp: string;
|
|
392
|
+
group_id: string;
|
|
393
|
+
group_openid: string;
|
|
394
|
+
message_scene?: {
|
|
395
|
+
source: string;
|
|
396
|
+
ext?: string[];
|
|
397
|
+
};
|
|
398
|
+
attachments?: MessageAttachment[];
|
|
399
|
+
/** @提及列表 */
|
|
400
|
+
mentions?: Array<{
|
|
401
|
+
scope?: "all" | "single";
|
|
402
|
+
id?: string;
|
|
403
|
+
user_openid?: string;
|
|
404
|
+
member_openid?: string;
|
|
405
|
+
nickname?: string;
|
|
406
|
+
bot?: boolean;
|
|
407
|
+
/** 是否 @机器人自身 */
|
|
408
|
+
is_you?: boolean;
|
|
409
|
+
}>;
|
|
410
|
+
/** 消息类型,参见 MSG_TYPE_* */
|
|
411
|
+
message_type?: number;
|
|
412
|
+
/** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
|
|
413
|
+
msg_elements?: MsgElement[];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 按钮交互事件(INTERACTION_CREATE)
|
|
417
|
+
*/
|
|
418
|
+
interface InteractionEvent {
|
|
419
|
+
/** 事件 ID,用于回应交互(PUT /interactions/{id}) */
|
|
420
|
+
id: string;
|
|
421
|
+
/** 事件类型:11=消息按钮 12=单聊快捷菜单 */
|
|
422
|
+
type: number;
|
|
423
|
+
/** 场景:c2c / group / guild */
|
|
424
|
+
scene?: string;
|
|
425
|
+
/** 场景类型:0=频道 1=群聊 2=单聊 */
|
|
426
|
+
chat_type?: number;
|
|
427
|
+
/** 触发时间 RFC3339 */
|
|
428
|
+
timestamp?: string;
|
|
429
|
+
/** 频道 openid(仅频道场景) */
|
|
430
|
+
guild_id?: string;
|
|
431
|
+
/** 子频道 openid(仅频道场景) */
|
|
432
|
+
channel_id?: string;
|
|
433
|
+
/** 单聊用户 openid(仅 c2c 场景) */
|
|
434
|
+
user_openid?: string;
|
|
435
|
+
/** 群 openid(仅群聊场景) */
|
|
436
|
+
group_openid?: string;
|
|
437
|
+
/** 群内触发用户 openid(仅群聊场景) */
|
|
438
|
+
group_member_openid?: string;
|
|
439
|
+
version: number;
|
|
440
|
+
data: {
|
|
441
|
+
type: number;
|
|
442
|
+
resolved: {
|
|
443
|
+
/** 按钮 action.data 值 */
|
|
444
|
+
button_data?: string;
|
|
445
|
+
/** 按钮 id */
|
|
446
|
+
button_id?: string;
|
|
447
|
+
/** 操作用户 userid(仅频道场景) */
|
|
448
|
+
user_id?: string;
|
|
449
|
+
/** 自定义菜单 id(仅菜单场景) */
|
|
450
|
+
feature_id?: string;
|
|
451
|
+
/** 操作的消息 id(仅频道场景) */
|
|
452
|
+
message_id?: string;
|
|
453
|
+
/** 配置更新:群消息模式 "mention"=@机器人时激活 "always"=总是激活 */
|
|
454
|
+
require_mention?: string;
|
|
455
|
+
/** 配置更新:群消息策略 */
|
|
456
|
+
group_policy?: GroupPolicy;
|
|
457
|
+
/** 配置更新:@文本的名称提及BOT名,多个使用,分隔 */
|
|
458
|
+
mention_patterns?: string;
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 按钮 Action 类型
|
|
464
|
+
* 0=跳转链接 1=回调型(INTERACTION_CREATE) 2=指令型(直接发文本) 3=mqqapi
|
|
465
|
+
*/
|
|
466
|
+
type KeyboardActionType = 0 | 1 | 2 | 3;
|
|
467
|
+
/** 按钮权限 */
|
|
468
|
+
interface KeyboardPermission {
|
|
469
|
+
/** 0=全体 1=管理员 2=按钮指定 3=身份组 */
|
|
470
|
+
type: 0 | 1 | 2 | 3;
|
|
471
|
+
specify_role_ids?: string[];
|
|
472
|
+
specify_user_ids?: string[];
|
|
473
|
+
}
|
|
474
|
+
/** 二次确认弹窗 */
|
|
475
|
+
interface KeyboardModal {
|
|
476
|
+
content: string;
|
|
477
|
+
confirm_text?: string;
|
|
478
|
+
cancel_text?: string;
|
|
479
|
+
}
|
|
480
|
+
/** 按钮 Action */
|
|
481
|
+
interface KeyboardAction {
|
|
482
|
+
type: KeyboardActionType;
|
|
483
|
+
data?: string;
|
|
484
|
+
/** true = 点击后直接发出(Enter)*/
|
|
485
|
+
enter?: boolean;
|
|
486
|
+
/** 仅指令型(type=2):是否把指令发到输入框(reply=true)还是静默发出 */
|
|
487
|
+
reply?: boolean;
|
|
488
|
+
permission?: KeyboardPermission;
|
|
489
|
+
click_limit?: number;
|
|
490
|
+
unsupport_tips?: string;
|
|
491
|
+
modal?: KeyboardModal;
|
|
492
|
+
}
|
|
493
|
+
/** 按钮渲染数据 */
|
|
494
|
+
interface KeyboardRenderData {
|
|
495
|
+
label: string;
|
|
496
|
+
visited_label?: string;
|
|
497
|
+
/** 0=灰色线框 1=蓝色线框 2=推荐回复专用 3=红色字体 4=蓝色背景 */
|
|
498
|
+
style?: 0 | 1 | 2 | 3 | 4;
|
|
499
|
+
}
|
|
500
|
+
/** 单个按钮 */
|
|
501
|
+
interface KeyboardButton {
|
|
502
|
+
id: string;
|
|
503
|
+
render_data?: KeyboardRenderData;
|
|
504
|
+
action?: KeyboardAction;
|
|
505
|
+
group_id?: string;
|
|
506
|
+
}
|
|
507
|
+
/** 一行按钮 */
|
|
508
|
+
interface KeyboardRow {
|
|
509
|
+
buttons: KeyboardButton[];
|
|
510
|
+
}
|
|
511
|
+
/** CustomKeyboard(自定义按钮内容) */
|
|
512
|
+
interface CustomKeyboard {
|
|
513
|
+
rows: KeyboardRow[];
|
|
514
|
+
}
|
|
515
|
+
/** MessageKeyboard(keyboard / prompt_keyboard.keyboard 共用) */
|
|
516
|
+
interface MessageKeyboard {
|
|
517
|
+
/** 模板 ID(与 content 二选一) */
|
|
518
|
+
id?: string;
|
|
519
|
+
/** 自定义内容 */
|
|
520
|
+
content?: CustomKeyboard;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Inline Keyboard(消息内嵌按钮,需平台审核)
|
|
524
|
+
* 发送字段:keyboard
|
|
525
|
+
* JSON: { "keyboard": { "id": "...", "content": { "rows": [...] } } }
|
|
526
|
+
*/
|
|
527
|
+
type InlineKeyboard = MessageKeyboard;
|
|
528
|
+
/**
|
|
529
|
+
* WebSocket 事件负载
|
|
530
|
+
*/
|
|
531
|
+
interface WSPayload {
|
|
532
|
+
op: number;
|
|
533
|
+
d?: unknown;
|
|
534
|
+
s?: number;
|
|
535
|
+
t?: string;
|
|
536
|
+
}
|
|
537
|
+
/** 流式消息输入模式 */
|
|
538
|
+
declare const StreamInputMode: {
|
|
539
|
+
/** 每次发送的 content_raw 替换整条消息内容 */
|
|
540
|
+
readonly REPLACE: "replace";
|
|
541
|
+
};
|
|
542
|
+
type StreamInputMode = (typeof StreamInputMode)[keyof typeof StreamInputMode];
|
|
543
|
+
/** 流式消息输入状态 */
|
|
544
|
+
declare const StreamInputState: {
|
|
545
|
+
/** 正文生成中 */
|
|
546
|
+
readonly GENERATING: 1;
|
|
547
|
+
/** 正文生成结束(终结状态) */
|
|
548
|
+
readonly DONE: 10;
|
|
549
|
+
};
|
|
550
|
+
type StreamInputState = (typeof StreamInputState)[keyof typeof StreamInputState];
|
|
551
|
+
/** 流式消息内容类型 */
|
|
552
|
+
declare const StreamContentType: {
|
|
553
|
+
readonly MARKDOWN: "markdown";
|
|
554
|
+
};
|
|
555
|
+
type StreamContentType = (typeof StreamContentType)[keyof typeof StreamContentType];
|
|
556
|
+
/**
|
|
557
|
+
* 流式消息请求体
|
|
558
|
+
* 对应 StreamReq proto
|
|
559
|
+
*/
|
|
560
|
+
interface StreamMessageRequest {
|
|
561
|
+
/** 输入模式 */
|
|
562
|
+
input_mode: StreamInputMode;
|
|
563
|
+
/** 输入状态 */
|
|
564
|
+
input_state: StreamInputState;
|
|
565
|
+
/** 内容类型 */
|
|
566
|
+
content_type: StreamContentType;
|
|
567
|
+
/** markdown 内容 */
|
|
568
|
+
content_raw: string;
|
|
569
|
+
/** 事件 ID */
|
|
570
|
+
event_id: string;
|
|
571
|
+
/** 原始消息 ID */
|
|
572
|
+
msg_id: string;
|
|
573
|
+
/** 流式消息 ID,首次发送后返回,后续分片需携带 */
|
|
574
|
+
stream_msg_id?: string;
|
|
575
|
+
/** 递增序号 */
|
|
576
|
+
msg_seq: number;
|
|
577
|
+
/** 同一条流式会话内的发送索引,从 0 开始,每次发送前递增;新流式会话重新从 0 开始 */
|
|
578
|
+
index: number;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* QQBot Channel Plugin
|
|
583
|
+
*/
|
|
584
|
+
declare const qqbotPlugin: openclaw_plugin_sdk_channel_core.ChannelPlugin<ResolvedQQBotAccount, unknown, unknown>;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* QQBot 插件运行时管理。
|
|
588
|
+
*
|
|
589
|
+
* 日志统一由 `utils/plugin-logger.ts` 提供,此处只管理 runtime 实例。
|
|
590
|
+
*/
|
|
591
|
+
|
|
592
|
+
declare function setQQBotRuntime(next: PluginRuntime): void;
|
|
593
|
+
declare function getQQBotRuntime(): PluginRuntime;
|
|
594
|
+
|
|
595
|
+
declare function buildUserAgent(suffix?: string): string;
|
|
596
|
+
/**
|
|
597
|
+
* 获取指定账户的 QQBot SDK 实例。
|
|
598
|
+
*
|
|
599
|
+
* @throws 如果该账户的 gateway 尚未启动
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```ts
|
|
603
|
+
* const bot = getBotForAccount(accountId);
|
|
604
|
+
* await bot.send({ target, content: 'hello' });
|
|
605
|
+
* const guilds = await bot.api.get('/users/@me/guilds');
|
|
606
|
+
* const token = await bot.api.getToken();
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
declare function getBotForAccount(accountId: string): QQBot;
|
|
610
|
+
/**
|
|
611
|
+
* 尝试获取指定账户的 QQBot SDK 实例(不抛异常)。
|
|
612
|
+
* 返回 null 表示 gateway 尚未启动。
|
|
613
|
+
*/
|
|
614
|
+
declare function tryGetBotForAccount(accountId: string): QQBot | null;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* PluginLogger — 统一日志接口
|
|
618
|
+
*
|
|
619
|
+
* 默认后端为 OpenClaw 框架 logger(`runtime.logging.getChildLogger`),
|
|
620
|
+
* 运行时不可用时临时降级 console,就绪后自动切换缓存。
|
|
621
|
+
*/
|
|
622
|
+
interface PluginLogger {
|
|
623
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
624
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
625
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
626
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
627
|
+
child(tag: string): PluginLogger;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* QQBotGateway — 封装单个 Bot 实例的完整生命周期
|
|
632
|
+
*/
|
|
633
|
+
|
|
634
|
+
interface GatewayCallbacks {
|
|
635
|
+
onReady?: () => void;
|
|
636
|
+
onError?: (error: Error) => void;
|
|
637
|
+
}
|
|
638
|
+
interface SendOptions {
|
|
639
|
+
msgId?: string;
|
|
640
|
+
text?: string;
|
|
641
|
+
}
|
|
642
|
+
declare class QQBotGateway {
|
|
643
|
+
readonly bot: QQBot;
|
|
644
|
+
private readonly account;
|
|
645
|
+
private readonly runtime;
|
|
646
|
+
readonly log: PluginLogger;
|
|
647
|
+
private readonly textTimeout;
|
|
648
|
+
private readonly mediaTimeout;
|
|
649
|
+
constructor(account: ResolvedQQBotAccount, runtime: PluginRuntime, log?: PluginLogger);
|
|
650
|
+
start(callbacks?: GatewayCallbacks, signal?: AbortSignal): Promise<void>;
|
|
651
|
+
stop(): Promise<void>;
|
|
652
|
+
sendText(target: ReplyTarget, text: string, opts?: SendOptions): Promise<MessageResponse>;
|
|
653
|
+
sendMedia(target: ReplyTarget, source: string, opts?: SendOptions & {
|
|
654
|
+
fileType?: MediaFileType;
|
|
655
|
+
}): Promise<MessageResponse>;
|
|
656
|
+
sendVoice(target: ReplyTarget, source: {
|
|
657
|
+
url?: string;
|
|
658
|
+
base64?: string;
|
|
659
|
+
localPath?: string;
|
|
660
|
+
}, opts?: SendOptions): Promise<MessageResponse>;
|
|
661
|
+
sendVideo(target: ReplyTarget, source: string, opts?: SendOptions): Promise<MessageResponse>;
|
|
662
|
+
sendFile(target: ReplyTarget, source: string, opts?: SendOptions & {
|
|
663
|
+
fileName?: string;
|
|
664
|
+
}): Promise<MessageResponse>;
|
|
665
|
+
openStream(target: ReplyTarget, msgId: string): StreamSession;
|
|
666
|
+
sendTyping(target: ReplyTarget): Promise<void>;
|
|
667
|
+
/**
|
|
668
|
+
* 消息发送成功后通知活跃的 typing 会话补发续期。
|
|
669
|
+
* QQ 客户端收到机器人消息(含思维链等中间输出)会终止"正在输入"
|
|
670
|
+
* 显示;若框架任务仍在进行,typing 中间件会在 5s 后补发恢复显示。
|
|
671
|
+
*/
|
|
672
|
+
private notifyTypingRefresh;
|
|
673
|
+
private wrapBotSendForRefIndex;
|
|
674
|
+
/**
|
|
675
|
+
* 配额感知的 msg_id 挂接(被动优先)。
|
|
676
|
+
*
|
|
677
|
+
* - 显式 msgId(opts.msgId):上游 adapter 已做配额记账(quotaReserved),
|
|
678
|
+
* 直接透传,不重复消费。
|
|
679
|
+
* - 无显式 msgId:尝试挂 msgid-cache 最新条目;挂接前经 quota-manager
|
|
680
|
+
* 原子预检+扣减——配额耗尽则不挂(降级主动,避免平台 40034128 硬失败),
|
|
681
|
+
* API 抛错时由调用方 rollback 释放已扣减的额度。
|
|
682
|
+
*/
|
|
683
|
+
private attachMsgIdWithQuota;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
type MediaKind = 'image' | 'voice' | 'video' | 'file';
|
|
687
|
+
interface SendResult {
|
|
688
|
+
messageId?: string;
|
|
689
|
+
error?: string;
|
|
690
|
+
errorCode?: string;
|
|
691
|
+
qqBizCode?: number;
|
|
692
|
+
}
|
|
693
|
+
declare function sendText(params: {
|
|
694
|
+
to: string;
|
|
695
|
+
text: string;
|
|
696
|
+
accountId?: string;
|
|
697
|
+
replyToId?: string;
|
|
698
|
+
account: ResolvedQQBotAccount;
|
|
699
|
+
quotaReserved?: boolean;
|
|
700
|
+
}): Promise<SendResult>;
|
|
701
|
+
declare function sendMedia(params: {
|
|
702
|
+
to: string;
|
|
703
|
+
text?: string;
|
|
704
|
+
mediaUrl: string;
|
|
705
|
+
mediaKind?: MediaKind;
|
|
706
|
+
accountId?: string;
|
|
707
|
+
replyToId?: string;
|
|
708
|
+
account: ResolvedQQBotAccount;
|
|
709
|
+
quotaReserved?: boolean;
|
|
710
|
+
}): Promise<SendResult>;
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* 出站目标地址解析
|
|
714
|
+
*
|
|
715
|
+
* 将 OpenClaw 规范的目标地址字符串(如 qqbot:c2c:xxx / qqbot:group:xxx)
|
|
716
|
+
* 转换为 SDK 的 ReplyTarget 结构。
|
|
717
|
+
*
|
|
718
|
+
* 也导出共享的正则常量供 channel.ts messaging 段复用。
|
|
719
|
+
*/
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* 解析目标地址字符串为 SDK ReplyTarget
|
|
723
|
+
*
|
|
724
|
+
* 注意:此函数总是返回 ReplyTarget,即使输入无效(targetId 可能为空字符串)
|
|
725
|
+
* 如果需要严格验证,请使用 tryParseTarget
|
|
726
|
+
*/
|
|
727
|
+
declare function parseTarget(to: string): ReplyTarget;
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* 消息转发 — 入站消息 → OpenClaw AI
|
|
731
|
+
*
|
|
732
|
+
* 核心职责:
|
|
733
|
+
* 1. 从 SDK MiddlewareContext 构建 OpenClaw 标准信封
|
|
734
|
+
* 2. 通过 runtime-adapter 将消息交给 AI 处理
|
|
735
|
+
*
|
|
736
|
+
* 架构说明:
|
|
737
|
+
* - 所有 runtime.channel.* 访问均通过 runtime-adapter 隔离
|
|
738
|
+
* - log: 前缀由 PluginLogger + 框架自动注入,消息体不重复 accountId
|
|
739
|
+
*/
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* 将经过中间件处理的入站消息转发给 OpenClaw AI
|
|
743
|
+
*/
|
|
744
|
+
declare function dispatchToOpenClaw(ctx: MiddlewareContext, msg: QQBotInboundMessage, account: ResolvedQQBotAccount, runtime: PluginRuntime, log?: PluginLogger): Promise<void>;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* 持久化 RefIndex 存储
|
|
748
|
+
*
|
|
749
|
+
* SDK 提供的 MemoryRefIndexStore 在进程重启后即丢失,
|
|
750
|
+
* 而 QQ 引用消息(REFIDX_xxx)入站事件只携带 key,必须本地缓存才能回查。
|
|
751
|
+
*
|
|
752
|
+
* 设计:
|
|
753
|
+
* - 内存 LRU(与 SDK 同款)保证 O(1) 读取
|
|
754
|
+
* - JSONL 追加写持久化,进程重启时按时间顺序回放重建 LRU
|
|
755
|
+
* - 写入触发 compact 阈值时重写文件(去重 + 截断到 maxEntries)
|
|
756
|
+
* - 文件路径: ~/.openclaw/qqbot/data/ref-index.jsonl
|
|
757
|
+
*
|
|
758
|
+
* 实现 SDK 的 RefIndexStore 接口,可直接通过 `quoteRef({ store })` 注入。
|
|
759
|
+
*/
|
|
760
|
+
|
|
761
|
+
interface PersistedRefIndexStoreOptions {
|
|
762
|
+
/** 内存与磁盘的最大条目数。默认 2000。 */
|
|
763
|
+
maxEntries?: number;
|
|
764
|
+
/** 自定义存储文件路径。默认 ~/.openclaw/qqbot/data/ref-index.jsonl */
|
|
765
|
+
filePath?: string;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* 持久化版本的 RefIndexStore
|
|
769
|
+
*
|
|
770
|
+
* - get:仅查内存(启动时从磁盘回放重建)
|
|
771
|
+
* - set:内存 + JSONL 追加写;磁盘行数过多时触发 compact
|
|
772
|
+
*/
|
|
773
|
+
declare class PersistedRefIndexStore implements RefIndexStore {
|
|
774
|
+
private readonly memory;
|
|
775
|
+
private readonly maxEntries;
|
|
776
|
+
private readonly filePath;
|
|
777
|
+
/** 当前磁盘累计写入的行数(用于 compact 阈值判断) */
|
|
778
|
+
private diskLineCount;
|
|
779
|
+
/** 是否已成功初始化(磁盘回放完成) */
|
|
780
|
+
private initialized;
|
|
781
|
+
/** 串行化写入,防止并发 append 撕裂行 */
|
|
782
|
+
private writeChain;
|
|
783
|
+
constructor(options?: PersistedRefIndexStoreOptions);
|
|
784
|
+
/**
|
|
785
|
+
* 初始化:按时间顺序回放 JSONL 重建内存 LRU
|
|
786
|
+
*/
|
|
787
|
+
private init;
|
|
788
|
+
get(key: string): RefEntry | undefined;
|
|
789
|
+
set(key: string, entry: RefEntry): void;
|
|
790
|
+
private touchMemory;
|
|
791
|
+
private appendToDisk;
|
|
792
|
+
/**
|
|
793
|
+
* 异步 compact:将内存 LRU 状态完整重写到磁盘,丢弃历史冗余。
|
|
794
|
+
*/
|
|
795
|
+
private compact;
|
|
796
|
+
/**
|
|
797
|
+
* 同步 compact(仅 init 阶段使用,避免回放后立刻保留巨大磁盘文件)
|
|
798
|
+
*/
|
|
799
|
+
private compactSync;
|
|
800
|
+
/** 当前内存中的条目数 */
|
|
801
|
+
get size(): number;
|
|
802
|
+
/** 是否已完成初始化(磁盘回放) */
|
|
803
|
+
get isInitialized(): boolean;
|
|
804
|
+
/** 诊断快照 */
|
|
805
|
+
stats(): {
|
|
806
|
+
memoryEntries: number;
|
|
807
|
+
diskLines: number;
|
|
808
|
+
maxEntries: number;
|
|
809
|
+
filePath: string;
|
|
810
|
+
};
|
|
811
|
+
/**
|
|
812
|
+
* 强制将当前内存状态持久化到磁盘(进程退出前调用)
|
|
813
|
+
*/
|
|
814
|
+
flush(): void;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* 获取按 accountId 隔离的持久化 RefIndexStore 单例。
|
|
818
|
+
*
|
|
819
|
+
* 每个账户独立存储文件,避免多账户混用同一个 refIdx 命名空间。
|
|
820
|
+
*/
|
|
821
|
+
declare function getPersistedRefIndexStore(accountId: string): PersistedRefIndexStore;
|
|
822
|
+
/**
|
|
823
|
+
* 进程退出前 flush 所有 store
|
|
824
|
+
*/
|
|
825
|
+
declare function flushAllRefIndexStores(): void;
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* QQ Bot 流式消息控制器
|
|
829
|
+
*
|
|
830
|
+
* 核心约束:QQ 流式 API 替换模式下,已下发文本的**前缀不可变更**。
|
|
831
|
+
*
|
|
832
|
+
* 状态机:
|
|
833
|
+
* IDLE → (first chunk) → STREAMING → (complete) → DONE
|
|
834
|
+
* → (prefix changed) → DONE
|
|
835
|
+
* → (new reply) → IDLE → …
|
|
836
|
+
*
|
|
837
|
+
* onPartialReply(text) 输入:模型全量文本,持续增长。
|
|
838
|
+
* finalize() 标记:框架通知回复结束,用 lastAccepted 收尾。
|
|
839
|
+
*/
|
|
840
|
+
|
|
841
|
+
type StreamingPhase = 'idle' | 'streaming' | 'done' | 'failed';
|
|
842
|
+
type StreamingSendMode = 'stream' | 'static';
|
|
843
|
+
interface StreamingControllerDeps {
|
|
844
|
+
gateway: QQBotGateway;
|
|
845
|
+
target: ReplyTarget;
|
|
846
|
+
accountId: string;
|
|
847
|
+
replyToId: string;
|
|
848
|
+
log?: PluginLogger;
|
|
849
|
+
/**
|
|
850
|
+
* 文本下发通道:
|
|
851
|
+
* - 'stream'(默认)QQ 流式打印机:openStream/update/complete
|
|
852
|
+
* - 'static' 流结束时用一条普通 sendText 发完整文本
|
|
853
|
+
* partial 接收逻辑(状态机/串行/去重)在两种模式下完全一致。
|
|
854
|
+
*/
|
|
855
|
+
sendMode?: StreamingSendMode;
|
|
856
|
+
/**
|
|
857
|
+
* static 模式专用:在 finalize 收尾时把累积的完整文本一次性发出。
|
|
858
|
+
* stream 模式下不使用。未提供时 static 模式将降级为 shouldFallbackToStatic。
|
|
859
|
+
*/
|
|
860
|
+
sendStatic?: (fullText: string) => Promise<void>;
|
|
861
|
+
}
|
|
862
|
+
declare class StreamingController {
|
|
863
|
+
private readonly deps;
|
|
864
|
+
private phase;
|
|
865
|
+
private session;
|
|
866
|
+
/** QQ 已接受的最新文本 — 单源真理 */
|
|
867
|
+
private lastAcceptedFull;
|
|
868
|
+
/** 已成功发送的分片数(降级:=0 则走静态消息兜底) */
|
|
869
|
+
private sentChunkCount;
|
|
870
|
+
/** 同步标志:收到第一个 onPartialReply 即置 true(不等 async 完成) */
|
|
871
|
+
private _hasStarted;
|
|
872
|
+
/** 串行队列 */
|
|
873
|
+
private chain;
|
|
874
|
+
constructor(deps: StreamingControllerDeps);
|
|
875
|
+
/** 是否走静态(普通 sendText)下发通道 */
|
|
876
|
+
private get isStaticMode();
|
|
877
|
+
/**
|
|
878
|
+
* 是否处于 static 下发模式(公开访问器,供 dispatch 协同判断)。
|
|
879
|
+
* static 模式下 dispatch 不应在 tool/final 事件提前 finalize,
|
|
880
|
+
* 由 controller 靠 new_reply 检测自主分段,避免终态与发送延迟。
|
|
881
|
+
*/
|
|
882
|
+
get isStaticSendMode(): boolean;
|
|
883
|
+
get currentPhase(): StreamingPhase;
|
|
884
|
+
/** 是否已成功发送至少一个流式分片 */
|
|
885
|
+
get hasSentChunks(): boolean;
|
|
886
|
+
/** 同步标志:流式已启动(不等异步完成),用于 final 去重 */
|
|
887
|
+
get hasStarted(): boolean;
|
|
888
|
+
get isTerminal(): boolean;
|
|
889
|
+
get shouldFallbackToStatic(): boolean;
|
|
890
|
+
onPartialReply(text: string): Promise<void>;
|
|
891
|
+
finalize(): Promise<void>;
|
|
892
|
+
/**
|
|
893
|
+
* static 模式:把当前累积段立即发出并重置缓冲,**不进入终态**。
|
|
894
|
+
* 供框架 onAssistantMessageStart 回调调用(工具调用后新一段推理开始时触发)。
|
|
895
|
+
* 对齐 telegram rotateLaneForNewMessage:固化旧段 + 开始新段。
|
|
896
|
+
* 无累积内容时跳过(第一段开始时 lastAcceptedFull 为空)。
|
|
897
|
+
*/
|
|
898
|
+
flushSegment(): Promise<void>;
|
|
899
|
+
abort(reason?: string): Promise<void>;
|
|
900
|
+
private handleChunk;
|
|
901
|
+
private handleFinalize;
|
|
902
|
+
private sendUpdate;
|
|
903
|
+
private completeSession;
|
|
904
|
+
private transition;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
declare function shouldUseStreaming(account: ResolvedQQBotAccount, targetScope: 'c2c' | 'group' | 'channel'): boolean;
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* 解析 mentionPatterns(agent → global → 空数组)
|
|
911
|
+
*
|
|
912
|
+
* 优先级:
|
|
913
|
+
* 1. agents.list[agentId].groupChat.mentionPatterns
|
|
914
|
+
* 2. messages.groupChat.mentionPatterns
|
|
915
|
+
* 3. []
|
|
916
|
+
*/
|
|
917
|
+
declare function resolveMentionPatterns(cfg: OpenClawConfig, agentId?: string): string[];
|
|
918
|
+
declare const DEFAULT_ACCOUNT_ID = "default";
|
|
919
|
+
/** 解析群消息策略 */
|
|
920
|
+
declare function resolveGroupPolicy(cfg: OpenClawConfig, accountId?: string): GroupPolicy$1;
|
|
921
|
+
/** 解析群白名单(统一转大写) */
|
|
922
|
+
declare function resolveGroupAllowFrom(cfg: OpenClawConfig, accountId?: string): string[];
|
|
923
|
+
/** 检查指定群是否被允许(使用标准策略引擎) */
|
|
924
|
+
declare function isGroupAllowed(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
|
|
925
|
+
type ResolvedGroupConfig = Omit<Required<GroupConfig>, "prompt" | "coalesce" | "historyMode" | "unmentionedInbound"> & {
|
|
926
|
+
prompt: string;
|
|
927
|
+
coalesce: Required<GroupCoalesceConfig>;
|
|
928
|
+
historyMode: 'clear' | 'rolling';
|
|
929
|
+
unmentionedInbound: 'user_request' | 'room_event';
|
|
930
|
+
};
|
|
931
|
+
declare function resolveGroupConfigFromAccount(account: ResolvedQQBotAccount, groupOpenid: string): ResolvedGroupConfig;
|
|
932
|
+
declare function resolveGroupConfig(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ResolvedGroupConfig;
|
|
933
|
+
/** 解析群历史消息缓存条数 */
|
|
934
|
+
declare function resolveHistoryLimit(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): number;
|
|
935
|
+
/** 解析群行为 PE(具体群 > "*" > 默认值) */
|
|
936
|
+
declare function resolveGroupPrompt(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): string;
|
|
937
|
+
/** 解析群是否需要 @机器人才响应 */
|
|
938
|
+
declare function resolveRequireMention(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
|
|
939
|
+
/** 解析群是否忽略 @了其他人(非 bot)的消息 */
|
|
940
|
+
declare function resolveIgnoreOtherMentions(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
|
|
941
|
+
/** 解析群工具策略 */
|
|
942
|
+
declare function resolveToolPolicy(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ToolPolicy;
|
|
943
|
+
/** 解析群名称(优先配置,fallback 为 openid 前 8 位) */
|
|
944
|
+
declare function resolveGroupName(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): string;
|
|
945
|
+
/**
|
|
946
|
+
* 解析 User-Agent 追加后缀(仅通道级:channels.qqbot.userAgentSuffix)
|
|
947
|
+
*/
|
|
948
|
+
declare function resolveUserAgentSuffix(cfg: OpenClawConfig): string;
|
|
949
|
+
/** 文档变量名优先,同时兼容旧的下划线变量名。 */
|
|
950
|
+
declare function resolveQQBotEnvAppId(): string;
|
|
951
|
+
declare function resolveQQBotEnvClientSecret(): string;
|
|
952
|
+
/**
|
|
953
|
+
* 列出所有 QQBot 账户 ID
|
|
954
|
+
*/
|
|
955
|
+
declare function listQQBotAccountIds(cfg: OpenClawConfig): string[];
|
|
956
|
+
/**
|
|
957
|
+
* 获取默认账户 ID
|
|
958
|
+
*/
|
|
959
|
+
declare function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string;
|
|
960
|
+
/**
|
|
961
|
+
* 解析 QQBot 账户配置
|
|
962
|
+
*/
|
|
963
|
+
declare function resolveQQBotAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedQQBotAccount;
|
|
964
|
+
/**
|
|
965
|
+
* 应用账户配置
|
|
966
|
+
*/
|
|
967
|
+
declare function applyQQBotAccountConfig(cfg: OpenClawConfig, accountId: string, input: {
|
|
968
|
+
appId?: string;
|
|
969
|
+
clientSecret?: string;
|
|
970
|
+
clientSecretFile?: string;
|
|
971
|
+
name?: string;
|
|
972
|
+
}): OpenClawConfig;
|
|
973
|
+
/** 解析群消息合并配置 */
|
|
974
|
+
declare function resolveGroupCoalesceConfig(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): Required<GroupCoalesceConfig>;
|
|
975
|
+
/** 解析群消息合并是否启用 */
|
|
976
|
+
declare function resolveGroupCoalesceEnabled(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* QQBot Outbound 适配器
|
|
980
|
+
*
|
|
981
|
+
* 封装 sendText/sendMedia 流式等出站操作,提供配额感知的发送能力。
|
|
982
|
+
*
|
|
983
|
+
* 两层结构:
|
|
984
|
+
* - createQQBotOutboundAdapter —— 内部配额包装(sendTextWithQuota / sendMediaWithQuota),
|
|
985
|
+
* 供中间件/流式路径使用;
|
|
986
|
+
* - createQQBotChannelOutbound —— 框架 ChannelOutboundAdapter 契约入口
|
|
987
|
+
* (sendText / sendMedia / chunker / sanitizeText 等)。
|
|
988
|
+
* openclaw 的 channel-selection 通过 outbound.sendText(或 message.send.text /
|
|
989
|
+
* deliveryMode === "gateway")判定通道可用,缺失会导致
|
|
990
|
+
* "Channel is unavailable: qqbot"。
|
|
991
|
+
*/
|
|
992
|
+
|
|
993
|
+
/** 框架 sendText / sendMedia 上下文(openclaw ChannelOutboundContext 子集) */
|
|
994
|
+
interface ChannelOutboundSendContext {
|
|
995
|
+
to: string;
|
|
996
|
+
text?: string;
|
|
997
|
+
mediaUrl?: string;
|
|
998
|
+
accountId?: string | null;
|
|
999
|
+
replyToId?: string | null;
|
|
1000
|
+
cfg: OpenClawConfig;
|
|
1001
|
+
}
|
|
1002
|
+
interface ChannelOutboundDeliveryResult {
|
|
1003
|
+
channel: 'qqbot';
|
|
1004
|
+
messageId: string;
|
|
1005
|
+
}
|
|
1006
|
+
declare const qqbotChannelOutbound: {
|
|
1007
|
+
deliveryMode: "direct";
|
|
1008
|
+
sendText: (ctx: ChannelOutboundSendContext) => Promise<ChannelOutboundDeliveryResult>;
|
|
1009
|
+
sendMedia: (ctx: ChannelOutboundSendContext) => Promise<ChannelOutboundDeliveryResult>;
|
|
1010
|
+
sanitizeText: ({ text }: {
|
|
1011
|
+
text: string;
|
|
1012
|
+
}) => string;
|
|
1013
|
+
chunker: (text: string, limit: number) => string[];
|
|
1014
|
+
chunkerMode: "markdown";
|
|
1015
|
+
textChunkLimit: number;
|
|
1016
|
+
shouldSuppressLocalPayloadPrompt: ({ payload }: {
|
|
1017
|
+
payload: unknown;
|
|
1018
|
+
}) => boolean;
|
|
1019
|
+
sendTextWithQuota: (params: {
|
|
1020
|
+
to: string;
|
|
1021
|
+
text: string;
|
|
1022
|
+
accountId?: string;
|
|
1023
|
+
replyToId?: string;
|
|
1024
|
+
account: ResolvedQQBotAccount;
|
|
1025
|
+
log?: PluginLogger;
|
|
1026
|
+
}) => Promise<{
|
|
1027
|
+
messageId?: string;
|
|
1028
|
+
error?: string;
|
|
1029
|
+
}>;
|
|
1030
|
+
sendMediaWithQuota: (params: {
|
|
1031
|
+
to: string;
|
|
1032
|
+
source: string;
|
|
1033
|
+
text?: string;
|
|
1034
|
+
accountId?: string;
|
|
1035
|
+
replyToId?: string;
|
|
1036
|
+
account: ResolvedQQBotAccount;
|
|
1037
|
+
agentId?: string;
|
|
1038
|
+
log?: PluginLogger;
|
|
1039
|
+
}) => Promise<{
|
|
1040
|
+
messageId?: string;
|
|
1041
|
+
error?: string;
|
|
1042
|
+
}>;
|
|
1043
|
+
canSendTyping: (params: {
|
|
1044
|
+
to: string;
|
|
1045
|
+
accountId: string;
|
|
1046
|
+
replyToId: string;
|
|
1047
|
+
log?: PluginLogger;
|
|
1048
|
+
}) => Promise<boolean>;
|
|
1049
|
+
shouldTreatDeliveredTextAsVisible: (params: {
|
|
1050
|
+
kind: string;
|
|
1051
|
+
text?: string;
|
|
1052
|
+
}) => boolean;
|
|
1053
|
+
preferFinalAssistantVisibleText: boolean;
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* 被动回复限额管理
|
|
1058
|
+
*
|
|
1059
|
+
* QQ Bot 被动回复有限额(同一条消息最多回复 N 次,超时后不能再被动回复)。
|
|
1060
|
+
* 当限额耗尽或消息过期时,标记应降级为主动消息(proactive)。
|
|
1061
|
+
*/
|
|
1062
|
+
interface ReplyLimiterConfig {
|
|
1063
|
+
/** 每条消息最大被动回复次数(默认 4) */
|
|
1064
|
+
limit?: number;
|
|
1065
|
+
/** 消息 ID 过期时间 ms(默认 1 小时) */
|
|
1066
|
+
ttlMs?: number;
|
|
1067
|
+
/** 最大跟踪消息数(LRU 驱逐,默认 10000) */
|
|
1068
|
+
maxTrackedMessages?: number;
|
|
1069
|
+
}
|
|
1070
|
+
interface ReplyLimitResult {
|
|
1071
|
+
allowed: boolean;
|
|
1072
|
+
remaining: number;
|
|
1073
|
+
shouldFallbackToProactive: boolean;
|
|
1074
|
+
fallbackReason?: 'expired' | 'limit_exceeded';
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* 被动回复限额管理器
|
|
1078
|
+
*
|
|
1079
|
+
* @example
|
|
1080
|
+
* ```ts
|
|
1081
|
+
* const limiter = new ReplyLimiter({ limit: 4, ttlMs: 3600_000 });
|
|
1082
|
+
* const result = limiter.checkLimit(messageId);
|
|
1083
|
+
* if (!result.allowed) {
|
|
1084
|
+
* // 降级为主动消息
|
|
1085
|
+
* } else {
|
|
1086
|
+
* limiter.record(messageId);
|
|
1087
|
+
* // 正常被动回复
|
|
1088
|
+
* }
|
|
1089
|
+
* ```
|
|
1090
|
+
*/
|
|
1091
|
+
declare class ReplyLimiter {
|
|
1092
|
+
private readonly limit;
|
|
1093
|
+
private readonly ttlMs;
|
|
1094
|
+
private readonly maxTracked;
|
|
1095
|
+
private readonly messages;
|
|
1096
|
+
constructor(config?: ReplyLimiterConfig);
|
|
1097
|
+
/**
|
|
1098
|
+
* 检查是否允许对指定消息继续被动回复
|
|
1099
|
+
*/
|
|
1100
|
+
checkLimit(messageId: string): ReplyLimitResult;
|
|
1101
|
+
/**
|
|
1102
|
+
* 记录一次被动回复
|
|
1103
|
+
*/
|
|
1104
|
+
record(messageId: string): void;
|
|
1105
|
+
/**
|
|
1106
|
+
* 尝试占用一个被动回复配额(检查 + 记录原子完成)。
|
|
1107
|
+
* 用于 typing 等非回复用途:占用失败时调用方应降级为主动发送(不带 msg_id)。
|
|
1108
|
+
*/
|
|
1109
|
+
tryAcquire(messageId: string): boolean;
|
|
1110
|
+
/**
|
|
1111
|
+
* 获取统计信息
|
|
1112
|
+
*/
|
|
1113
|
+
getStats(): {
|
|
1114
|
+
trackedMessages: number;
|
|
1115
|
+
totalReplies: number;
|
|
1116
|
+
};
|
|
1117
|
+
/**
|
|
1118
|
+
* 清除所有跟踪数据
|
|
1119
|
+
*/
|
|
1120
|
+
clear(): void;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* 配额管理参数
|
|
1125
|
+
*/
|
|
1126
|
+
interface QuotaCheckParams {
|
|
1127
|
+
accountId: string;
|
|
1128
|
+
msgId?: string;
|
|
1129
|
+
scope: 'c2c' | 'group';
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* QQBot 被动回复配额管理
|
|
1134
|
+
*
|
|
1135
|
+
* C2C: 4 次/msg_id, 60 分钟
|
|
1136
|
+
* Group: 5 次/msg_id, 5 分钟
|
|
1137
|
+
*
|
|
1138
|
+
* 重要说明:
|
|
1139
|
+
* - QQ Bot 平台的 msg_id 有时效性限制(C2C: 60分钟,Group: 5分钟)
|
|
1140
|
+
* - 过期后的 msg_id 不能再用于被动回复(API 会返回错误 40034128)
|
|
1141
|
+
* - 这是平台限制,不是"配额恢复"
|
|
1142
|
+
* - 因此 checkPassiveReplyQuota 在 msg_id 过期时返回 false,而非重置配额
|
|
1143
|
+
*
|
|
1144
|
+
* 使用方式:
|
|
1145
|
+
* 1. 推荐:使用 checkAndConsumePassiveReplyQuota 进行原子操作(失败用 rollback 回滚)
|
|
1146
|
+
* 2. 纯探测(不消耗配额):checkPassiveReplyQuota
|
|
1147
|
+
*/
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* 检查被动回复配额(纯探测,不消耗配额)。
|
|
1151
|
+
* 需要同时消耗配额时使用 checkAndConsumePassiveReplyQuota。
|
|
1152
|
+
*/
|
|
1153
|
+
declare function checkPassiveReplyQuota(params: QuotaCheckParams): boolean;
|
|
1154
|
+
declare function inferQQBotScope(to: string): 'c2c' | 'group';
|
|
1155
|
+
declare function clearQuotaCache(): void;
|
|
1156
|
+
declare function getQuotaStats(accountId: string, scope: 'c2c' | 'group'): {
|
|
1157
|
+
activeSessions: number;
|
|
1158
|
+
totalUsage: number;
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* C2C "正在输入"指示器中间件(替代 SDK 的 typingIndicator)。
|
|
1163
|
+
*
|
|
1164
|
+
* 与 SDK 版的三点差异:
|
|
1165
|
+
* 1. 续期间隔默认 20s 且强制不低于 20s(QPS 约束,事件触发的续期
|
|
1166
|
+
* 同样受此间距保护)
|
|
1167
|
+
* 2. 配额感知:typing 通知携带入站 msg_id 时属于被动回复、消耗该
|
|
1168
|
+
* msg_id 的被动配额,与真正的回复消息共享额度(经同一 limiter
|
|
1169
|
+
* 记账)。被动配额耗尽后与回复消息一样自动降级为主动发送
|
|
1170
|
+
* (不带 msg_id),续期不会中断
|
|
1171
|
+
* 3. 出站消息后续期:QQ 客户端收到机器人消息(如思维链中间输出)
|
|
1172
|
+
* 会自动终止"正在输入"显示。若框架任务仍在进行,本中间件在
|
|
1173
|
+
* 每条消息发出 POST_MESSAGE_REFRESH_DELAY_MS 后补发一次续期;
|
|
1174
|
+
* 若该消息是最终回复(处理链完成),会话已结束,自然不再续期
|
|
1175
|
+
*
|
|
1176
|
+
* QQ 客户端行为:退出聊天界面再进入后指示器会消失,只有收到新的
|
|
1177
|
+
* input_notify 推送才会重新显示 —— 这是需要周期性续期的原因。
|
|
1178
|
+
*/
|
|
1179
|
+
|
|
1180
|
+
/** QPS 约束:续期间隔不得低于 20s */
|
|
1181
|
+
declare const MIN_TYPING_INTERVAL_MS = 20000;
|
|
1182
|
+
/** 出站消息后延迟多久补发续期 */
|
|
1183
|
+
declare const POST_MESSAGE_REFRESH_DELAY_MS = 5000;
|
|
1184
|
+
declare function resolveTypingIntervalMs(input: number | undefined): number;
|
|
1185
|
+
interface TypingIndicatorMiddlewareOptions {
|
|
1186
|
+
accountId: string;
|
|
1187
|
+
/** 续期间隔 ms,默认 20_000,低于 20_000 会被钳制到 20_000 */
|
|
1188
|
+
intervalMs?: number;
|
|
1189
|
+
}
|
|
1190
|
+
declare function c2cTypingIndicator(opts: TypingIndicatorMiddlewareOptions): Middleware;
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* 出站消息 → typing 续期信号
|
|
1194
|
+
*
|
|
1195
|
+
* QQ 客户端收到机器人消息后会自动终止"正在输入"显示。若框架任务
|
|
1196
|
+
* 仍在进行(如思维链等中间消息),typing 中间件需要在消息发出后
|
|
1197
|
+
* 延迟数秒补发一次续期,恢复指示器显示。
|
|
1198
|
+
*
|
|
1199
|
+
* 本模块提供进程内信号:出站层(QQBotGateway)在消息发送成功后
|
|
1200
|
+
* notify,活跃的 typing 会话按 accountId + scope + targetId 订阅。
|
|
1201
|
+
* 任务已完成的会话已取消订阅,notify 自然不产生任何效果。
|
|
1202
|
+
*/
|
|
1203
|
+
type Listener = () => void;
|
|
1204
|
+
/**
|
|
1205
|
+
* 订阅指定会话的出站消息信号。
|
|
1206
|
+
* @returns 取消订阅函数
|
|
1207
|
+
*/
|
|
1208
|
+
declare function subscribeOutboundMessage(accountId: string, scope: string, targetId: string, listener: Listener): () => void;
|
|
1209
|
+
/**
|
|
1210
|
+
* 消息发送成功后由出站层调用:通知该会话活跃的 typing 会话补发续期。
|
|
1211
|
+
*/
|
|
1212
|
+
declare function notifyOutboundMessageSent(accountId: string, scope: string, targetId: string): void;
|
|
1213
|
+
|
|
1214
|
+
declare const plugin: {
|
|
1215
|
+
id: string;
|
|
1216
|
+
name: string;
|
|
1217
|
+
description: string;
|
|
1218
|
+
configSchema: openclaw_plugin_sdk_core.OpenClawPluginConfigSchema;
|
|
1219
|
+
register(api: OpenClawPluginApi): void;
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
export { type AudioFormatPolicy, type C2CMessageEvent, type CommandsConfig, type CustomKeyboard, DEFAULT_ACCOUNT_ID, type DeliverDebounceConfig, type GroupCoalesceConfig, type GroupConfig, type GroupMessageEvent, type GroupPolicy, type GuildMessageEvent, type InlineKeyboard, type InteractionEvent, type KeyboardAction, type KeyboardActionType, type KeyboardButton, type KeyboardModal, type KeyboardPermission, type KeyboardRenderData, type KeyboardRow, MIN_TYPING_INTERVAL_MS, MSG_TYPE_QUOTE, MSG_TYPE_TEXT, type MessageAttachment, type MessageKeyboard, type MsgElement, POST_MESSAGE_REFRESH_DELAY_MS, PersistedRefIndexStore, type QQBotAccountConfig, type QQBotConfig, QQBotGateway, type RateLimitConfig, type RateLimitTierConfig, ReplyLimiter, type ResolvedGroupConfig, type ResolvedQQBotAccount, type STTChannelConfig, StreamContentType, StreamInputMode, StreamInputState, type StreamMessageRequest, StreamingController, type ToolPolicy, type TransportMode, type TypingIndicatorConfig, type WSPayload, type WebhookTransportConfig, applyQQBotAccountConfig, buildUserAgent, c2cTypingIndicator, checkPassiveReplyQuota, clearQuotaCache, plugin as default, dispatchToOpenClaw, flushAllRefIndexStores, getBotForAccount, getPersistedRefIndexStore, getQQBotRuntime, getQuotaStats, inferQQBotScope, isGroupAllowed, listQQBotAccountIds, notifyOutboundMessageSent, parseTarget, qqbotChannelOutbound, qqbotPlugin, resolveDefaultQQBotAccountId, resolveGroupAllowFrom, resolveGroupCoalesceConfig, resolveGroupCoalesceEnabled, resolveGroupConfig, resolveGroupConfigFromAccount, resolveGroupName, resolveGroupPolicy, resolveGroupPrompt, resolveHistoryLimit, resolveIgnoreOtherMentions, resolveMentionPatterns, resolveQQBotAccount, resolveQQBotEnvAppId, resolveQQBotEnvClientSecret, resolveRequireMention, resolveToolPolicy, resolveTypingIntervalMs, resolveUserAgentSuffix, sendMedia, sendText, setQQBotRuntime, shouldUseStreaming, subscribeOutboundMessage, tryGetBotForAccount };
|