@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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBot Typing 续期管理
|
|
3
|
+
*
|
|
4
|
+
* 续期时机:50 秒后
|
|
5
|
+
* 最大续期:10 次(约 10 分钟)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TypingParams, TypingState } from './types-plugin.js';
|
|
9
|
+
import { checkAndConsumePassiveReplyQuota, inferQQBotScope } from './features/quota-manager.js';
|
|
10
|
+
|
|
11
|
+
const activeTypingSessions = new Map<string, TypingState>();
|
|
12
|
+
|
|
13
|
+
const TYPING_RENEWAL_MS = 50 * 1000;
|
|
14
|
+
const MAX_RENEWALS = 10;
|
|
15
|
+
|
|
16
|
+
export async function startTypingWithRenewal(
|
|
17
|
+
params: TypingParams & {
|
|
18
|
+
sendTyping: (params: { to: string; msgId?: string }) => Promise<boolean>;
|
|
19
|
+
},
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const { accountId, to, replyToId, log, sendTyping } = params;
|
|
22
|
+
const sessionKey = `${accountId}:${to}:${replyToId}`;
|
|
23
|
+
|
|
24
|
+
if (activeTypingSessions.has(sessionKey)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const scope = inferQQBotScope(to);
|
|
29
|
+
|
|
30
|
+
if (scope !== 'c2c') {
|
|
31
|
+
log?.debug?.(`[${accountId}] typing not supported for scope: ${scope}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!replyToId) {
|
|
36
|
+
log?.debug?.(`[${accountId}] typing requires replyToId`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { canReply: canPassiveReply, rollback } = checkAndConsumePassiveReplyQuota({
|
|
41
|
+
accountId,
|
|
42
|
+
msgId: replyToId,
|
|
43
|
+
scope: 'c2c',
|
|
44
|
+
log,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const msgIdToSend = canPassiveReply ? replyToId : undefined;
|
|
48
|
+
|
|
49
|
+
if (!canPassiveReply) {
|
|
50
|
+
log?.debug?.(`[${accountId}] typing falling back to proactive mode: quota exhausted`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sent = await sendTyping({ to, msgId: msgIdToSend });
|
|
54
|
+
if (!sent) {
|
|
55
|
+
rollback();
|
|
56
|
+
log?.debug?.(`[${accountId}] typing start failed: send failed`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
log?.debug?.(`[${accountId}] typing started: ${sessionKey}`);
|
|
61
|
+
|
|
62
|
+
const timer = setTimeout(async () => {
|
|
63
|
+
await handleTypingRenewal({
|
|
64
|
+
sessionKey,
|
|
65
|
+
accountId,
|
|
66
|
+
to,
|
|
67
|
+
replyToId,
|
|
68
|
+
log,
|
|
69
|
+
sendTyping,
|
|
70
|
+
});
|
|
71
|
+
}, TYPING_RENEWAL_MS);
|
|
72
|
+
|
|
73
|
+
activeTypingSessions.set(sessionKey, {
|
|
74
|
+
timer,
|
|
75
|
+
startedAt: Date.now(),
|
|
76
|
+
renewalCount: 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleTypingRenewal(
|
|
81
|
+
params: TypingParams & {
|
|
82
|
+
sessionKey: string;
|
|
83
|
+
sendTyping: (params: { to: string; msgId?: string }) => Promise<boolean>;
|
|
84
|
+
},
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const { sessionKey, accountId, to, replyToId, log, sendTyping } = params;
|
|
87
|
+
|
|
88
|
+
const state = activeTypingSessions.get(sessionKey);
|
|
89
|
+
if (!state) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (state.renewalCount >= MAX_RENEWALS) {
|
|
94
|
+
log?.debug?.(`[${accountId}] typing stopped: max renewals reached`);
|
|
95
|
+
activeTypingSessions.delete(sessionKey);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { canReply: canPassiveReply, rollback } = checkAndConsumePassiveReplyQuota({
|
|
100
|
+
accountId,
|
|
101
|
+
msgId: replyToId,
|
|
102
|
+
scope: 'c2c',
|
|
103
|
+
log,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const msgIdToSend = canPassiveReply ? replyToId : undefined;
|
|
107
|
+
|
|
108
|
+
if (!canPassiveReply) {
|
|
109
|
+
log?.debug?.(`[${accountId}] typing renewal falling back to proactive mode: quota exhausted`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const renewed = await sendTyping({ to, msgId: msgIdToSend });
|
|
113
|
+
if (!renewed) {
|
|
114
|
+
rollback();
|
|
115
|
+
log?.debug?.(`[${accountId}] typing renewal failed: send failed`);
|
|
116
|
+
activeTypingSessions.delete(sessionKey);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
state.renewalCount += 1;
|
|
121
|
+
log?.debug?.(`[${accountId}] typing renewed #${state.renewalCount}: ${sessionKey}`);
|
|
122
|
+
|
|
123
|
+
state.timer = setTimeout(async () => {
|
|
124
|
+
await handleTypingRenewal(params);
|
|
125
|
+
}, TYPING_RENEWAL_MS);
|
|
126
|
+
|
|
127
|
+
activeTypingSessions.set(sessionKey, state);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function stopTyping(params: {
|
|
131
|
+
accountId: string;
|
|
132
|
+
to: string;
|
|
133
|
+
replyToId: string;
|
|
134
|
+
log?: {
|
|
135
|
+
debug?: (message: string) => void;
|
|
136
|
+
};
|
|
137
|
+
}): void {
|
|
138
|
+
const { accountId, to, replyToId, log } = params;
|
|
139
|
+
const sessionKey = `${accountId}:${to}:${replyToId}`;
|
|
140
|
+
|
|
141
|
+
const state = activeTypingSessions.get(sessionKey);
|
|
142
|
+
if (state) {
|
|
143
|
+
clearTimeout(state.timer);
|
|
144
|
+
activeTypingSessions.delete(sessionKey);
|
|
145
|
+
log?.debug?.(`[${accountId}] typing stopped: ${sessionKey}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function cleanupAllTyping(): void {
|
|
150
|
+
for (const [, state] of activeTypingSessions.entries()) {
|
|
151
|
+
clearTimeout(state.timer);
|
|
152
|
+
}
|
|
153
|
+
activeTypingSessions.clear();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function isTypingActive(accountId: string, replyToId: string): boolean {
|
|
157
|
+
for (const [key] of activeTypingSessions.entries()) {
|
|
158
|
+
if (key.startsWith(`${accountId}:`) && key.endsWith(`:${replyToId}`)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 清理指定账户的所有 typing session
|
|
167
|
+
* 在账户停止时调用,防止旧 session 阻塞新 session
|
|
168
|
+
*/
|
|
169
|
+
export function cleanupTypingByAccount(accountId: string): void {
|
|
170
|
+
const keysToDelete: string[] = [];
|
|
171
|
+
|
|
172
|
+
for (const [key, state] of activeTypingSessions.entries()) {
|
|
173
|
+
if (key.startsWith(`${accountId}:`)) {
|
|
174
|
+
clearTimeout(state.timer);
|
|
175
|
+
keysToDelete.push(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const key of keysToDelete) {
|
|
180
|
+
activeTypingSessions.delete(key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention 工具函数
|
|
3
|
+
*
|
|
4
|
+
* @mention 检测与清理,供 channel.ts 和 gateway 层使用。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface MentionEntry {
|
|
8
|
+
member_openid?: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
user_openid?: string;
|
|
11
|
+
is_you?: boolean;
|
|
12
|
+
nickname?: string;
|
|
13
|
+
username?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 清理 @mention:替换 <@openid> 为 @用户名,去除 @机器人自身 */
|
|
17
|
+
export function stripMentionText(text: string, mentions?: MentionEntry[]): string {
|
|
18
|
+
if (!text || !mentions?.length) return text;
|
|
19
|
+
let cleaned = text;
|
|
20
|
+
for (const m of mentions) {
|
|
21
|
+
const openid = m.member_openid ?? m.id ?? m.user_openid;
|
|
22
|
+
if (!openid) continue;
|
|
23
|
+
if (m.is_you) {
|
|
24
|
+
cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, 'g'), '').trim();
|
|
25
|
+
} else {
|
|
26
|
+
const displayName = m.nickname ?? m.username;
|
|
27
|
+
if (displayName) {
|
|
28
|
+
cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, 'g'), `@${displayName}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return cleaned;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 检测消息是否 @了机器人 */
|
|
36
|
+
export function detectWasMentioned({ eventType, mentions, content, mentionPatterns }: {
|
|
37
|
+
eventType?: string;
|
|
38
|
+
mentions?: Array<{ is_you?: boolean }>;
|
|
39
|
+
content?: string;
|
|
40
|
+
mentionPatterns?: string[];
|
|
41
|
+
}): boolean {
|
|
42
|
+
if (mentions?.some((m) => m.is_you)) return true;
|
|
43
|
+
if (eventType === 'GROUP_AT_MESSAGE_CREATE') return true;
|
|
44
|
+
if (mentionPatterns?.length && content) {
|
|
45
|
+
for (const pattern of mentionPatterns) {
|
|
46
|
+
try {
|
|
47
|
+
if (new RegExp(pattern, 'i').test(content)) return true;
|
|
48
|
+
} catch { /* 无效正则,跳过 */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本号获取工具。
|
|
3
|
+
*
|
|
4
|
+
* - getPackageVersion(): 插件自身版本(@jerryliang122/openclaw-qqbot)
|
|
5
|
+
* - getOpenclawVersion(): OpenClaw 框架版本(PluginRuntime.version)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare const __PLUGIN_VERSION__: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 获取插件自身版本号。
|
|
12
|
+
* 编译时由 tsup define 注入,零运行时 IO。
|
|
13
|
+
*/
|
|
14
|
+
export function getPackageVersion(): string {
|
|
15
|
+
return typeof __PLUGIN_VERSION__ !== 'undefined' ? __PLUGIN_VERSION__ : 'unknown';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* OpenClaw 框架版本(PluginRuntime.version,peer >=2026.9.2 恒存在)。
|
|
20
|
+
*/
|
|
21
|
+
export function getOpenclawVersion(runtimeVersion?: string): string {
|
|
22
|
+
return runtimeVersion && runtimeVersion !== 'unknown' ? runtimeVersion : 'unknown';
|
|
23
|
+
}
|