@jerryliang122/openclaw-qqbot 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +7343 -7210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/package.json +1 -1
- package/src/adapter/pairing.ts +14 -1
- package/src/adapter/webhook.ts +16 -3
- package/src/commands/bot-approve.ts +2 -2
- package/src/commands/bot-group-always.ts +1 -1
- package/src/commands/bot-streaming.ts +1 -1
- package/src/commands/config-util.ts +8 -0
- package/src/commands/index.ts +49 -12
- package/src/dispatch/dispatch.ts +1 -1
- package/src/features/approval-capability.ts +19 -6
- package/src/features/credential-backup.ts +8 -3
- package/src/features/history-store.ts +8 -1
- package/src/features/quota-manager.ts +5 -2
- package/src/features/typing-refresh.ts +9 -1
- package/src/gateway/event-handlers.ts +36 -12
- package/src/gateway/lifecycle.ts +8 -0
- package/src/gateway/qqbot-gateway.ts +24 -1
- package/src/middleware/access-control.ts +3 -1
- package/src/middleware/secret-capture.ts +5 -3
- package/src/outbound/debounce.ts +19 -2
- package/src/outbound-adapter.ts +10 -1
- package/src/setup/finalize.ts +3 -2
- package/src/tools/platform.ts +8 -0
- package/src/tools/secret-input.ts +8 -0
- package/src/types-plugin.ts +1 -0
- package/src/typing-lifecycle.ts +9 -4
|
@@ -49,14 +49,16 @@ async function replyToUser(
|
|
|
49
49
|
try {
|
|
50
50
|
await ctx.bot.sendText(ctx.replyTarget, text);
|
|
51
51
|
return;
|
|
52
|
-
} catch {
|
|
52
|
+
} catch (err) {
|
|
53
53
|
rollback();
|
|
54
|
-
|
|
54
|
+
ctx.log?.warn?.(`[secret] passive reply failed, falling back to proactive: ${err instanceof Error ? err.message : String(err)}`);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const proactiveTarget = { scope: 'c2c' as const, targetId: ctx.message.senderId as string };
|
|
59
|
-
await ctx.bot.sendText(proactiveTarget, text).catch(() => {
|
|
59
|
+
await ctx.bot.sendText(proactiveTarget, text).catch((err: unknown) => {
|
|
60
|
+
ctx.log?.warn?.(`[secret] proactive reply failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
function formatSetFailure(name: string, result: SecretsStoreSetResult): string {
|
package/src/outbound/debounce.ts
CHANGED
|
@@ -13,6 +13,9 @@ const DEFAULT_WINDOW_MS = 1500;
|
|
|
13
13
|
const DEFAULT_MAX_WAIT_MS = 8000;
|
|
14
14
|
const DEFAULT_SEPARATOR = '\n\n---\n\n';
|
|
15
15
|
|
|
16
|
+
// 最小结构类型:兼容 PluginLogger 与 SDK Logger(与 config-util 同款)
|
|
17
|
+
type WarnSink = { warn?: (msg: string, meta?: Record<string, unknown>) => void };
|
|
18
|
+
|
|
16
19
|
interface PendingDeliver {
|
|
17
20
|
texts: string[];
|
|
18
21
|
firstAt: number;
|
|
@@ -26,15 +29,18 @@ export class DeliverDebouncer {
|
|
|
26
29
|
private readonly maxWaitMs: number;
|
|
27
30
|
private readonly separator: string;
|
|
28
31
|
private readonly flush: (targetId: string, mergedText: string) => Promise<void>;
|
|
32
|
+
private readonly log?: WarnSink;
|
|
29
33
|
|
|
30
34
|
constructor(
|
|
31
35
|
config: DeliverDebounceConfig | undefined,
|
|
32
36
|
flush: (targetId: string, mergedText: string) => Promise<void>,
|
|
37
|
+
log?: WarnSink,
|
|
33
38
|
) {
|
|
34
39
|
this.windowMs = config?.windowMs ?? DEFAULT_WINDOW_MS;
|
|
35
40
|
this.maxWaitMs = config?.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
36
41
|
this.separator = config?.separator ?? DEFAULT_SEPARATOR;
|
|
37
42
|
this.flush = flush;
|
|
43
|
+
this.log = log;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
get enabled(): boolean {
|
|
@@ -60,7 +66,7 @@ export class DeliverDebouncer {
|
|
|
60
66
|
if (Date.now() - existing.firstAt >= this.maxWaitMs) {
|
|
61
67
|
await this.doFlush(targetId);
|
|
62
68
|
} else {
|
|
63
|
-
existing.timer = setTimeout(() => this.
|
|
69
|
+
existing.timer = setTimeout(() => this.scheduledFlush(targetId), this.windowMs);
|
|
64
70
|
}
|
|
65
71
|
return;
|
|
66
72
|
}
|
|
@@ -70,13 +76,24 @@ export class DeliverDebouncer {
|
|
|
70
76
|
const pending: PendingDeliver = {
|
|
71
77
|
texts: [text],
|
|
72
78
|
firstAt: Date.now(),
|
|
73
|
-
timer: setTimeout(() => this.
|
|
79
|
+
timer: setTimeout(() => this.scheduledFlush(targetId), this.windowMs),
|
|
74
80
|
resolve,
|
|
75
81
|
};
|
|
76
82
|
this.pending.set(targetId, pending);
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 定时器触发的 flush:promise 被 setTimeout 回调丢弃,失败若不就地
|
|
88
|
+
* 捕获会成为 unhandledRejection——记 WARN 封口(pending.resolve 在
|
|
89
|
+
* doFlush 的 finally 中仍会执行,入队方不会被悬挂)。
|
|
90
|
+
*/
|
|
91
|
+
private scheduledFlush(targetId: string): void {
|
|
92
|
+
void this.doFlush(targetId).catch((err: unknown) => {
|
|
93
|
+
this.log?.warn?.(`[debounce] timer flush failed target=${targetId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
private async doFlush(targetId: string): Promise<void> {
|
|
81
98
|
const pending = this.pending.get(targetId);
|
|
82
99
|
if (!pending) return;
|
package/src/outbound-adapter.ts
CHANGED
|
@@ -28,6 +28,11 @@ import { isApprovalPayload } from './features/approval-utils.js';
|
|
|
28
28
|
import { TEXT_CHUNK_LIMIT } from './constants.js';
|
|
29
29
|
import { tryGetQQBotRuntime } from './runtime.js';
|
|
30
30
|
import { getAdapters } from './adapter/resolve.js';
|
|
31
|
+
import { createPluginLogger } from './utils/plugin-logger.js';
|
|
32
|
+
|
|
33
|
+
// 契约入口(框架直发 / message 工具 / cron announce)无调用方 logger 可传,
|
|
34
|
+
// 用模块级 logger 兜底——frameworkSink 每次调用时 resolve,register 早期自动降级 console
|
|
35
|
+
const alog = createPluginLogger({ prefix: '[outbound]' });
|
|
31
36
|
|
|
32
37
|
export type SendTextFn = (params: {
|
|
33
38
|
to: string;
|
|
@@ -178,6 +183,8 @@ export function createQQBotOutboundAdapter(params: QQBotOutboundAdapterParams):
|
|
|
178
183
|
if (canReply && replyToId && result.error) {
|
|
179
184
|
rollback();
|
|
180
185
|
log?.debug?.(`[${resolvedAccountId}] rollback quota: send failed`);
|
|
186
|
+
} else if (!canReply) {
|
|
187
|
+
log?.info?.(`[${resolvedAccountId}] [quota] fallback to proactive media send: quota exhausted or no msgId`);
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
return result;
|
|
@@ -287,6 +294,7 @@ export function createQQBotChannelOutbound(params: QQBotOutboundAdapterParams =
|
|
|
287
294
|
accountId: ctx.accountId ?? undefined,
|
|
288
295
|
replyToId: resolvePassiveFirstReplyToId(ctx.to, ctx.replyToId),
|
|
289
296
|
account,
|
|
297
|
+
log: alog,
|
|
290
298
|
});
|
|
291
299
|
if (result.error) throw new Error(result.error);
|
|
292
300
|
return { channel: 'qqbot', messageId: result.messageId ?? '' };
|
|
@@ -302,7 +310,8 @@ export function createQQBotChannelOutbound(params: QQBotOutboundAdapterParams =
|
|
|
302
310
|
accountId: resolvedAccountId,
|
|
303
311
|
replyToId: resolvePassiveFirstReplyToId(ctx.to, ctx.replyToId),
|
|
304
312
|
account,
|
|
305
|
-
agentId: resolveMCPAgentId(ctx.to, resolvedAccountId, ctx.cfg),
|
|
313
|
+
agentId: resolveMCPAgentId(ctx.to, resolvedAccountId, ctx.cfg, alog),
|
|
314
|
+
log: alog,
|
|
306
315
|
});
|
|
307
316
|
if (result.error) throw new Error(result.error);
|
|
308
317
|
return { channel: 'qqbot', messageId: result.messageId ?? '' };
|
package/src/setup/finalize.ts
CHANGED
|
@@ -49,13 +49,14 @@ async function linkViaQrCode(cfg: OpenClawConfig, _accountId: string, prompter:
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async function linkViaManual(cfg: OpenClawConfig, _accountId: string, prompter: Prompter): Promise<OpenClawConfig> {
|
|
52
|
+
async function linkViaManual(cfg: OpenClawConfig, _accountId: string, prompter: Prompter, rt: Runtime): Promise<OpenClawConfig> {
|
|
53
53
|
const appIdInput = await prompter.text({ message: '请输入 QQ Bot AppID', validate: (v) => v.trim() ? undefined : 'AppID 不能为空' });
|
|
54
54
|
const secret = await prompter.text({ message: '请输入 QQ Bot AppSecret', validate: (v) => v.trim() ? undefined : 'AppSecret 不能为空' });
|
|
55
55
|
const appId = appIdInput.trim();
|
|
56
56
|
const key = resolveAccountKey(cfg, appId);
|
|
57
57
|
let next = applyQQBotAccountConfig(cfg, key, { appId, clientSecret: secret.trim() });
|
|
58
58
|
next = applyAccountDefaults(next, key);
|
|
59
|
+
rt.log(`手动绑定完成!账户: ${key} (AppID: ${appId})`);
|
|
59
60
|
await prompter.note('✔ QQ Bot 配置完成!', 'QQ Bot');
|
|
60
61
|
return next;
|
|
61
62
|
}
|
|
@@ -82,7 +83,7 @@ export async function finalizeQQBotSetup(params: {
|
|
|
82
83
|
if (mode === 'qr') {
|
|
83
84
|
next = await linkViaQrCode(next, accountId, params.prompter, params.runtime);
|
|
84
85
|
} else if (mode === 'manual') {
|
|
85
|
-
next = await linkViaManual(next, accountId, params.prompter);
|
|
86
|
+
next = await linkViaManual(next, accountId, params.prompter, params.runtime);
|
|
86
87
|
} else if (!configured) {
|
|
87
88
|
await params.prompter.note('您可以稍后运行以下命令重新配置:\n openclaw channels add', 'QQ Bot');
|
|
88
89
|
}
|
package/src/tools/platform.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
|
2
2
|
import { listQQBotAccountIds } from '../config.js';
|
|
3
3
|
import { getBotForAccount } from '../bot-instance.js';
|
|
4
4
|
import { getRequestAccountId } from '../request-context.js';
|
|
5
|
+
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
6
|
+
|
|
7
|
+
// 工具 execute 无 logger 参数;request-context 活跃期间 enrichMeta
|
|
8
|
+
// 自动带上 accountId/openId/messageId
|
|
9
|
+
const toolLog = createPluginLogger({ prefix: '[tool:platform]' });
|
|
5
10
|
|
|
6
11
|
// ========== JSON Schema ==========
|
|
7
12
|
|
|
@@ -111,6 +116,7 @@ export function registerPlatformTool(api: OpenClawPluginApi): void {
|
|
|
111
116
|
try {
|
|
112
117
|
const bot = getBotForAccount(accountId);
|
|
113
118
|
const apiGateway = bot.api;
|
|
119
|
+
const startedAt = Date.now();
|
|
114
120
|
|
|
115
121
|
let data: unknown;
|
|
116
122
|
switch (method) {
|
|
@@ -131,10 +137,12 @@ export function registerPlatformTool(api: OpenClawPluginApi): void {
|
|
|
131
137
|
break;
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
toolLog.info(`${method} ${p.path} ok (${Date.now() - startedAt}ms)`, { accountId });
|
|
134
141
|
return json(data);
|
|
135
142
|
} catch (err: unknown) {
|
|
136
143
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
137
144
|
const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
|
|
145
|
+
toolLog.warn(`${method} ${p.path} failed status=${apiErr.httpStatus ?? '?'} code=${apiErr.bizCode ?? '?'}: ${errMsg}`, { accountId });
|
|
138
146
|
return json({
|
|
139
147
|
error: errMsg,
|
|
140
148
|
status: apiErr.httpStatus,
|
|
@@ -22,8 +22,13 @@ import {
|
|
|
22
22
|
} from '../features/secret-input-store.js';
|
|
23
23
|
import { getRequestAccountId, getRequestTarget } from '../request-context.js';
|
|
24
24
|
import { parseTarget } from '../outbound/target.js';
|
|
25
|
+
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
25
26
|
import type { InlineKeyboard, KeyboardButton } from '../types.js';
|
|
26
27
|
|
|
28
|
+
// 工具 execute 无 logger 参数可传;request-context 在工具执行期间处于活跃,
|
|
29
|
+
// 模块级 logger 的 enrichMeta 会自动带上 accountId/openId/messageId
|
|
30
|
+
const toolLog = createPluginLogger({ prefix: '[tool:secret-input]' });
|
|
31
|
+
|
|
27
32
|
// ========== JSON Schema ==========
|
|
28
33
|
|
|
29
34
|
const SecretInputSchema = {
|
|
@@ -162,11 +167,14 @@ export function registerSecretInputTool(api: OpenClawPluginApi): void {
|
|
|
162
167
|
);
|
|
163
168
|
} catch (err) {
|
|
164
169
|
cancelPendingSecretInput(accountId, parsed.targetId);
|
|
170
|
+
toolLog.error(`card send failed: ${err instanceof Error ? err.message : String(err)}`, { name, accountId });
|
|
165
171
|
return json({
|
|
166
172
|
error: `密钥输入卡片发送失败:${err instanceof Error ? err.message : String(err)}`,
|
|
167
173
|
});
|
|
168
174
|
}
|
|
169
175
|
|
|
176
|
+
toolLog.info(`card sent, pending registered name=${name} ttlMinutes=${DEFAULT_SECRET_INPUT_TTL_MS / 60_000}`);
|
|
177
|
+
|
|
170
178
|
return json({
|
|
171
179
|
ok: true,
|
|
172
180
|
name,
|
package/src/types-plugin.ts
CHANGED
package/src/typing-lifecycle.ts
CHANGED
|
@@ -59,14 +59,17 @@ export async function startTypingWithRenewal(
|
|
|
59
59
|
|
|
60
60
|
log?.debug?.(`[${accountId}] typing started: ${sessionKey}`);
|
|
61
61
|
|
|
62
|
-
const timer = setTimeout(
|
|
63
|
-
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
// promise 被 setTimeout 回调丢弃,失败不就地捕获会成为 unhandledRejection
|
|
64
|
+
void handleTypingRenewal({
|
|
64
65
|
sessionKey,
|
|
65
66
|
accountId,
|
|
66
67
|
to,
|
|
67
68
|
replyToId,
|
|
68
69
|
log,
|
|
69
70
|
sendTyping,
|
|
71
|
+
}).catch((err: unknown) => {
|
|
72
|
+
log?.warn?.(`[${accountId}] typing renewal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
73
|
});
|
|
71
74
|
}, TYPING_RENEWAL_MS);
|
|
72
75
|
|
|
@@ -120,8 +123,10 @@ async function handleTypingRenewal(
|
|
|
120
123
|
state.renewalCount += 1;
|
|
121
124
|
log?.debug?.(`[${accountId}] typing renewed #${state.renewalCount}: ${sessionKey}`);
|
|
122
125
|
|
|
123
|
-
state.timer = setTimeout(
|
|
124
|
-
|
|
126
|
+
state.timer = setTimeout(() => {
|
|
127
|
+
void handleTypingRenewal(params).catch((err: unknown) => {
|
|
128
|
+
log?.warn?.(`[${accountId}] typing renewal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
});
|
|
125
130
|
}, TYPING_RENEWAL_MS);
|
|
126
131
|
|
|
127
132
|
activeTypingSessions.set(sessionKey, state);
|