@jerryliang122/openclaw-qqbot 1.0.8 → 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 +15 -28
- package/README.zh.md +15 -28
- package/dist/index.cjs +264 -247
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -7
- package/package.json +1 -1
- package/src/middleware/attachment.ts +30 -19
- package/src/openclaw-plugin-sdk.d.ts +38 -2
- package/src/tools/platform.ts +86 -72
- package/src/tools/remind.ts +53 -42
- package/src/tools/secret-input.ts +112 -80
- package/src/tools/tool-session.ts +74 -0
- package/src/types.ts +21 -7
- package/src/utils/stt.ts +49 -92
package/dist/index.d.cts
CHANGED
|
@@ -203,8 +203,9 @@ interface QQBotAccountConfig {
|
|
|
203
203
|
sendMode?: 'stream' | 'static';
|
|
204
204
|
};
|
|
205
205
|
/**
|
|
206
|
-
* STT (语音转文字)
|
|
207
|
-
*
|
|
206
|
+
* STT (语音转文字) 行为开关
|
|
207
|
+
* 转录凭证统一走框架 tools.media.audio.models;
|
|
208
|
+
* 框架未配置时直接使用 QQ 平台转写(asr_refer_text)
|
|
208
209
|
*/
|
|
209
210
|
stt?: STTChannelConfig;
|
|
210
211
|
/**
|
|
@@ -289,17 +290,30 @@ interface AudioFormatPolicy {
|
|
|
289
290
|
}
|
|
290
291
|
/**
|
|
291
292
|
* STT (语音转文字) 配置
|
|
293
|
+
*
|
|
294
|
+
* 2026-10 起转录统一走框架音频理解管线(tools.media.audio.models 凭证),
|
|
295
|
+
* 本块只保留行为开关;旧凭证键已废弃(检测到会打迁移提示日志)。
|
|
296
|
+
* 框架 STT 未配置时,QQ 平台转写(asr_refer_text)直接作为唯一来源。
|
|
292
297
|
*/
|
|
293
298
|
interface STTChannelConfig {
|
|
294
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* 是否启用框架 STT 转录(默认 true)。
|
|
301
|
+
* false = 不调用外部 STT,语音只用平台转写(或无转写时占位文本)。
|
|
302
|
+
*/
|
|
295
303
|
enabled?: boolean;
|
|
296
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* 平台转写(asr_refer_text)参与开关。默认参与:
|
|
306
|
+
* 框架 STT 未配置时直接作为唯一来源,STT 失败/为空时兜底。
|
|
307
|
+
* 设为 false 恢复严格模式——所有场景丢弃平台转写。
|
|
308
|
+
*/
|
|
309
|
+
asrFallback?: boolean;
|
|
310
|
+
/** @deprecated 2026-10 起忽略——STT 凭证统一配置在框架 tools.media.audio.models */
|
|
297
311
|
provider?: string;
|
|
298
|
-
/**
|
|
312
|
+
/** @deprecated 同上 */
|
|
299
313
|
baseUrl?: string;
|
|
300
|
-
/**
|
|
314
|
+
/** @deprecated 同上 */
|
|
301
315
|
apiKey?: string;
|
|
302
|
-
/**
|
|
316
|
+
/** @deprecated 同上 */
|
|
303
317
|
model?: string;
|
|
304
318
|
}
|
|
305
319
|
/**
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
isVoiceAttachment,
|
|
16
16
|
} from '@tencent-connect/qqbot-nodejs/protocol';
|
|
17
17
|
import type { MessageAttachment } from '../types.js';
|
|
18
|
-
import {
|
|
18
|
+
import { shouldUsePlatformAsr, isFrameworkSttConfigured, hasLegacySttCredentials, transcribeAudioViaFramework } from '../utils/stt.js';
|
|
19
19
|
import { formatVoiceText, formatDuration, type VoiceTranscript, type TranscriptSource } from '../utils/voice-text.js';
|
|
20
20
|
import { downloadRemoteMedia } from '../adapter/media.js';
|
|
21
21
|
import { getAdapters } from '../adapter/resolve.js';
|
|
@@ -96,8 +96,9 @@ export async function processAttachments(
|
|
|
96
96
|
cfg: Record<string, unknown>,
|
|
97
97
|
log?: Log,
|
|
98
98
|
): Promise<ProcessedAttachments> {
|
|
99
|
-
const sttCfg = resolveSTTConfig(cfg);
|
|
100
99
|
const usePlatformAsr = shouldUsePlatformAsr(cfg);
|
|
100
|
+
const sttConfigured = isFrameworkSttConfigured(cfg);
|
|
101
|
+
warnLegacySttCredentials(cfg, log);
|
|
101
102
|
const audioPolicy = resolveAudioPolicy(cfg);
|
|
102
103
|
|
|
103
104
|
const imageUrls: string[] = [];
|
|
@@ -120,7 +121,7 @@ export async function processAttachments(
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
if (isVoice) {
|
|
123
|
-
const transcript = await processVoiceAttachment(att,
|
|
124
|
+
const transcript = await processVoiceAttachment(att, cfg, usePlatformAsr, sttConfigured, audioPolicy, log);
|
|
124
125
|
return { type: 'voice' as const, transcript };
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -208,29 +209,42 @@ function kindFromContentType(contentType: string | undefined): InboundMediaEntry
|
|
|
208
209
|
|
|
209
210
|
// ── 语音处理 ──
|
|
210
211
|
|
|
212
|
+
/** 废弃插件级 STT 凭证的一次性迁移提示(每进程一条,避免每条语音刷屏) */
|
|
213
|
+
let legacySttWarned = false;
|
|
214
|
+
|
|
215
|
+
function warnLegacySttCredentials(cfg: Record<string, unknown>, log?: Log): void {
|
|
216
|
+
if (!legacySttWarned && hasLegacySttCredentials(cfg)) {
|
|
217
|
+
legacySttWarned = true;
|
|
218
|
+
log?.info(
|
|
219
|
+
'Voice: channels.qqbot.stt credentials (provider/baseUrl/apiKey/model) are deprecated and ignored; ' +
|
|
220
|
+
'configure tools.media.audio.models instead — platform asr_refer_text is used when framework STT is absent',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
async function processVoiceAttachment(
|
|
212
226
|
att: MessageAttachment,
|
|
213
|
-
|
|
227
|
+
cfg: Record<string, unknown>,
|
|
214
228
|
usePlatformAsr: boolean,
|
|
229
|
+
sttConfigured: boolean,
|
|
215
230
|
audioPolicy: AudioPolicyResolved,
|
|
216
231
|
log?: Log,
|
|
217
232
|
): Promise<VoiceTranscript> {
|
|
218
|
-
// 平台转写(asr_refer_text
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
// 下载失败回退)一并堵死。
|
|
233
|
+
// 平台转写(asr_refer_text,QQ 平台自动 STT 随事件 JSON 下发):
|
|
234
|
+
// 默认参与——框架 STT 未配置时直接作为唯一来源,STT 失败时兜底;
|
|
235
|
+
// 仅 asrFallback: false(严格模式)时在所有场景丢弃。
|
|
222
236
|
const rawAsrText = att.asr_refer_text?.trim() || undefined;
|
|
223
237
|
const asrReferText = usePlatformAsr ? rawAsrText : undefined;
|
|
224
238
|
// 远端 URL 兜底:优先 wav_url,其次原始 url
|
|
225
239
|
const remoteUrl = normalizeUrl(att.voice_wav_url) || normalizeUrl(att.url) || undefined;
|
|
226
240
|
|
|
227
|
-
// STT
|
|
228
|
-
if (!
|
|
241
|
+
// 框架 STT 未配置 → 平台转写直接作为 transcript(无下载、无外部调用)
|
|
242
|
+
if (!sttConfigured) {
|
|
229
243
|
if (!usePlatformAsr && rawAsrText) {
|
|
230
|
-
log?.info(`Voice: STT not configured; platform asr_refer_text discarded (asrFallback
|
|
244
|
+
log?.info(`Voice: framework STT not configured; platform asr_refer_text discarded (asrFallback: false)`);
|
|
231
245
|
}
|
|
232
246
|
if (asrReferText) {
|
|
233
|
-
log?.debug?.(`Voice: using asr_refer_text (STT not configured
|
|
247
|
+
log?.debug?.(`Voice: using platform asr_refer_text (framework STT not configured)`);
|
|
234
248
|
return { text: asrReferText, source: 'asr', asrReferText, remoteUrl };
|
|
235
249
|
}
|
|
236
250
|
return {
|
|
@@ -281,17 +295,18 @@ async function processVoiceAttachment(
|
|
|
281
295
|
|
|
282
296
|
if (localPath) {
|
|
283
297
|
try {
|
|
284
|
-
const transcript = await
|
|
298
|
+
const transcript = await transcribeAudioViaFramework(localPath, cfg);
|
|
285
299
|
if (transcript) {
|
|
286
|
-
log?.debug?.(`Voice STT: ${transcript.slice(0, 80)}...`);
|
|
300
|
+
log?.debug?.(`Voice STT (framework): ${transcript.slice(0, 80)}...`);
|
|
287
301
|
return { text: transcript, source: 'stt', duration, localPath, remoteUrl, asrReferText };
|
|
288
302
|
}
|
|
289
303
|
} catch (err) {
|
|
290
|
-
log?.error(`Voice STT failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
304
|
+
log?.error(`Voice STT (framework) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
291
305
|
}
|
|
292
306
|
}
|
|
293
307
|
|
|
294
308
|
if (asrReferText) {
|
|
309
|
+
log?.debug?.(`Voice: falling back to platform asr_refer_text after framework STT failure`);
|
|
295
310
|
return { text: asrReferText, source: 'asr', duration, localPath, remoteUrl, asrReferText };
|
|
296
311
|
}
|
|
297
312
|
|
|
@@ -334,10 +349,6 @@ function normalizeFormats(formats: string[]): string[] {
|
|
|
334
349
|
});
|
|
335
350
|
}
|
|
336
351
|
|
|
337
|
-
function cfg2stt(sttCfg: NonNullable<ReturnType<typeof resolveSTTConfig>>): Record<string, unknown> {
|
|
338
|
-
return { channels: { qqbot: { stt: sttCfg } } };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
352
|
// ── 文件工具 ──
|
|
342
353
|
|
|
343
354
|
function normalizeUrl(url: string | undefined): string {
|
|
@@ -77,6 +77,32 @@ declare module "openclaw/plugin-sdk" {
|
|
|
77
77
|
execute: (toolCallId: string, params: unknown) => Promise<AgentToolResult> | AgentToolResult;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 工具工厂上下文(registerTool 工厂形式)
|
|
82
|
+
*
|
|
83
|
+
* 框架在装配当前 run 的工具面(或缓存描述符路径下的每次执行)时调用
|
|
84
|
+
* 工厂,ctx 携带 run 级会话信息——其中 deliveryContext 是框架按当前
|
|
85
|
+
* run 的 sessionCtx 派生的可信投递路由(2026-09-23 修复:延迟/排队
|
|
86
|
+
* turn 中 ALS 请求上下文失效时,工具由此回退解析会话目标)。
|
|
87
|
+
* 仅声明本项目实际使用的字段,其余字段经索引签名透传。
|
|
88
|
+
*/
|
|
89
|
+
export interface PluginToolFactoryContext {
|
|
90
|
+
config?: OpenClawConfig;
|
|
91
|
+
runtimeConfig?: OpenClawConfig;
|
|
92
|
+
agentId?: string;
|
|
93
|
+
sessionKey?: string;
|
|
94
|
+
sessionId?: string;
|
|
95
|
+
messageChannel?: string;
|
|
96
|
+
agentAccountId?: string;
|
|
97
|
+
/** 可信投递路由:{ channel, to, accountId, threadId? } */
|
|
98
|
+
deliveryContext?: { channel?: string; to?: string; accountId?: string; threadId?: string };
|
|
99
|
+
/** 当前入站 turn 的平台原生会话 ID */
|
|
100
|
+
nativeChannelId?: string;
|
|
101
|
+
/** 当前入站 turn 的发送者 ID(运行时提供,非工具参数) */
|
|
102
|
+
requesterSenderId?: string;
|
|
103
|
+
[key: string]: unknown;
|
|
104
|
+
}
|
|
105
|
+
|
|
80
106
|
/**
|
|
81
107
|
* Tool 注册选项
|
|
82
108
|
*/
|
|
@@ -103,8 +129,18 @@ declare module "openclaw/plugin-sdk" {
|
|
|
103
129
|
};
|
|
104
130
|
/** 注册频道 */
|
|
105
131
|
registerChannel<TAccount = unknown>(options: { plugin: ChannelPlugin<TAccount> }): void;
|
|
106
|
-
/**
|
|
107
|
-
|
|
132
|
+
/**
|
|
133
|
+
* 注册工具。支持两种形式:
|
|
134
|
+
* - 静态定义(AnyAgentTool)
|
|
135
|
+
* - 工厂函数:每次工具装配/执行时由框架调用,ctx 携带 run 级
|
|
136
|
+
* deliveryContext 等会话信息(推荐需要感知当前会话的工具使用)
|
|
137
|
+
*/
|
|
138
|
+
registerTool(
|
|
139
|
+
tool:
|
|
140
|
+
| AnyAgentTool
|
|
141
|
+
| ((ctx: PluginToolFactoryContext) => AnyAgentTool | AnyAgentTool[] | null | undefined),
|
|
142
|
+
opts?: ToolRegistrationOptions,
|
|
143
|
+
): void;
|
|
108
144
|
/** 其他 API 方法 */
|
|
109
145
|
[key: string]: unknown;
|
|
110
146
|
}
|
package/src/tools/platform.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
2
2
|
import { listQQBotAccountIds } from '../config.js';
|
|
3
|
-
import {
|
|
4
|
-
import { getRequestAccountId } from '../request-context.js';
|
|
3
|
+
import { resolveGatewayForSend } from '../outbound/outbound-service.js';
|
|
5
4
|
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
5
|
+
import { resolveToolSessionRoute, type ToolDeliveryContext } from './tool-session.js';
|
|
6
6
|
|
|
7
7
|
// 工具 execute 无 logger 参数;request-context 活跃期间 enrichMeta
|
|
8
8
|
// 自动带上 accountId/openId/messageId
|
|
@@ -71,6 +71,8 @@ function validatePath(path: string): string | null {
|
|
|
71
71
|
* - 频道(Guild/Channel/Member/Announce/Forum/Schedule)→ 见 qqbot-channel skill
|
|
72
72
|
* - 群(Group/Member)→ 见 qqbot-group skill
|
|
73
73
|
*/
|
|
74
|
+
// 工厂注册形式(2026-09-23 修复):turn 被框架延迟/排队执行(ALS 失效)时,
|
|
75
|
+
// 从 deliveryContext 回退解析账户(见 tool-session.ts)。
|
|
74
76
|
export function registerPlatformTool(api: OpenClawPluginApi): void {
|
|
75
77
|
const cfg = api.config;
|
|
76
78
|
if (!cfg) return;
|
|
@@ -79,78 +81,90 @@ export function registerPlatformTool(api: OpenClawPluginApi): void {
|
|
|
79
81
|
if (accountIds.length === 0) return;
|
|
80
82
|
|
|
81
83
|
api.registerTool(
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let data: unknown;
|
|
122
|
-
switch (method) {
|
|
123
|
-
case 'GET':
|
|
124
|
-
data = await apiGateway.get(p.path, p.query);
|
|
125
|
-
break;
|
|
126
|
-
case 'POST':
|
|
127
|
-
data = await apiGateway.post(p.path, p.body);
|
|
128
|
-
break;
|
|
129
|
-
case 'PUT':
|
|
130
|
-
data = await apiGateway.put(p.path, p.body);
|
|
131
|
-
break;
|
|
132
|
-
case 'PATCH':
|
|
133
|
-
data = await apiGateway.patch(p.path, p.body);
|
|
134
|
-
break;
|
|
135
|
-
case 'DELETE':
|
|
136
|
-
data = await apiGateway.delete(p.path);
|
|
137
|
-
break;
|
|
84
|
+
(factoryCtx) => {
|
|
85
|
+
const delivery = (factoryCtx as { deliveryContext?: ToolDeliveryContext }).deliveryContext;
|
|
86
|
+
return {
|
|
87
|
+
name: 'qqbot_platform_api',
|
|
88
|
+
label: 'QQBot Platform API Gateway',
|
|
89
|
+
description:
|
|
90
|
+
'QQ 开放平台统一 HTTP API 网关,自动填充鉴权 Token。' +
|
|
91
|
+
'常用接口速查:' +
|
|
92
|
+
'【频道】GET /users/@me/guilds | /guilds/{guild_id}/channels | /channels/{channel_id} | ' +
|
|
93
|
+
'【群】GET /v2/groups/{group_id}/bot_state | /v2/groups/{group_id}/members/{member_id} | /v2/groups/{group_id}/info。' +
|
|
94
|
+
'更多接口和参数详情请阅读 qqbot-channel 和 qqbot-group skill。',
|
|
95
|
+
parameters: PlatformApiSchema,
|
|
96
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
97
|
+
const p = params as {
|
|
98
|
+
method: string;
|
|
99
|
+
path: string;
|
|
100
|
+
body?: Record<string, unknown>;
|
|
101
|
+
query?: Record<string, string>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (!p.method) return json({ error: 'method 为必填参数' });
|
|
105
|
+
if (!p.path) return json({ error: 'path 为必填参数' });
|
|
106
|
+
|
|
107
|
+
const method = p.method.toUpperCase();
|
|
108
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
109
|
+
return json({ error: `不支持的 HTTP 方法: ${method}` });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pathError = validatePath(p.path);
|
|
113
|
+
if (pathError) return json({ error: pathError });
|
|
114
|
+
|
|
115
|
+
// 会话路由:ALS 优先,deliveryContext 回退(延迟/排队 turn)
|
|
116
|
+
const route = resolveToolSessionRoute(delivery);
|
|
117
|
+
if (!route) {
|
|
118
|
+
return json({
|
|
119
|
+
error:
|
|
120
|
+
'当前运行没有 QQ 会话来源(可能由定时任务或框架内部触发的 run),' +
|
|
121
|
+
'无法确定调用哪个 QQ Bot 账户。请在 QQ 消息会话中使用本工具。',
|
|
122
|
+
});
|
|
138
123
|
}
|
|
139
124
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
125
|
+
try {
|
|
126
|
+
const resolvedGateway = resolveGatewayForSend(route.accountId ?? 'default');
|
|
127
|
+
if (!resolvedGateway) {
|
|
128
|
+
return json({ error: `账户 ${route.accountId ?? 'default'} 的 gateway 尚未启动` });
|
|
129
|
+
}
|
|
130
|
+
const bot = resolvedGateway.gw.bot;
|
|
131
|
+
const apiGateway = bot.api;
|
|
132
|
+
const startedAt = Date.now();
|
|
133
|
+
|
|
134
|
+
let data: unknown;
|
|
135
|
+
switch (method) {
|
|
136
|
+
case 'GET':
|
|
137
|
+
data = await apiGateway.get(p.path, p.query);
|
|
138
|
+
break;
|
|
139
|
+
case 'POST':
|
|
140
|
+
data = await apiGateway.post(p.path, p.body);
|
|
141
|
+
break;
|
|
142
|
+
case 'PUT':
|
|
143
|
+
data = await apiGateway.put(p.path, p.body);
|
|
144
|
+
break;
|
|
145
|
+
case 'PATCH':
|
|
146
|
+
data = await apiGateway.patch(p.path, p.body);
|
|
147
|
+
break;
|
|
148
|
+
case 'DELETE':
|
|
149
|
+
data = await apiGateway.delete(p.path);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
toolLog.info(`${method} ${p.path} ok (${Date.now() - startedAt}ms)`, { accountId: resolvedGateway.accountId });
|
|
154
|
+
return json(data);
|
|
155
|
+
} catch (err: unknown) {
|
|
156
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
157
|
+
const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
|
|
158
|
+
toolLog.warn(`${method} ${p.path} failed status=${apiErr.httpStatus ?? '?'} code=${apiErr.bizCode ?? '?'}: ${errMsg}`, { accountId: route.accountId ?? 'default' });
|
|
159
|
+
return json({
|
|
160
|
+
error: errMsg,
|
|
161
|
+
status: apiErr.httpStatus,
|
|
162
|
+
code: apiErr.bizCode,
|
|
163
|
+
path: p.path,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
};
|
|
154
168
|
},
|
|
155
169
|
{ name: 'qqbot_platform_api' },
|
|
156
170
|
);
|
package/src/tools/remind.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveToolSessionRoute, type ToolDeliveryContext } from "./tool-session.js";
|
|
3
3
|
|
|
4
4
|
// ========== 类型定义 ==========
|
|
5
5
|
|
|
@@ -218,52 +218,62 @@ function formatDelay(ms: number): string {
|
|
|
218
218
|
|
|
219
219
|
// ========== 注册入口 ==========
|
|
220
220
|
|
|
221
|
+
// 工厂注册形式(2026-09-23 修复):turn 被框架延迟/排队执行(ALS 失效)时,
|
|
222
|
+
// 从 deliveryContext 回退解析投递目标(见 tool-session.ts)。
|
|
221
223
|
export function registerRemindTool(api: OpenClawPluginApi): void {
|
|
222
224
|
api.registerTool(
|
|
223
|
-
{
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
225
|
+
(factoryCtx) => {
|
|
226
|
+
const delivery = (factoryCtx as { deliveryContext?: ToolDeliveryContext }).deliveryContext;
|
|
227
|
+
return {
|
|
228
|
+
name: "qqbot_remind",
|
|
229
|
+
label: "QQBot 定时提醒",
|
|
230
|
+
description:
|
|
231
|
+
"创建、查询、删除 QQ 定时提醒。" +
|
|
232
|
+
"使用简单参数即可,无需手动构造 cron JSON。\n" +
|
|
233
|
+
"创建提醒:action=add, content=提醒内容, to=目标地址, time=时间\n" +
|
|
234
|
+
"查看提醒:action=list\n" +
|
|
235
|
+
"删除提醒:action=remove, jobId=任务ID(先 list 获取)\n" +
|
|
236
|
+
"时间格式示例:\"5m\"(5分钟后) \"1h\"(1小时后) \"0 8 * * *\"(每天8点)",
|
|
237
|
+
parameters: RemindSchema,
|
|
238
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
239
|
+
const p = params as RemindParams;
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
// ===== list =====
|
|
242
|
+
if (p.action === "list") {
|
|
243
|
+
return json({
|
|
244
|
+
_instruction: "请立即使用 cron 工具执行以下参数",
|
|
245
|
+
cronParams: { action: "list" },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
244
248
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
// ===== remove =====
|
|
250
|
+
if (p.action === "remove") {
|
|
251
|
+
if (!p.jobId) {
|
|
252
|
+
return json({ error: "action=remove 时 jobId 为必填参数。请先用 action=list 获取 jobId。" });
|
|
253
|
+
}
|
|
254
|
+
return json({
|
|
255
|
+
_instruction: "请立即使用 cron 工具执行以下参数",
|
|
256
|
+
cronParams: { action: "remove", jobId: p.jobId },
|
|
257
|
+
});
|
|
249
258
|
}
|
|
250
|
-
return json({
|
|
251
|
-
_instruction: "请立即使用 cron 工具执行以下参数",
|
|
252
|
-
cronParams: { action: "remove", jobId: p.jobId },
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
259
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
// ===== add =====
|
|
261
|
+
if (!p.content) {
|
|
262
|
+
return json({ error: "action=add 时 content(提醒内容)为必填参数" });
|
|
263
|
+
}
|
|
264
|
+
// 优先使用 AI 传入的 to;否则双层解析当前会话路由:
|
|
265
|
+
// ALS 请求级上下文优先,框架 run 级 deliveryContext 回退
|
|
266
|
+
const route = resolveToolSessionRoute(delivery);
|
|
267
|
+
const resolvedTo = p.to || route?.target;
|
|
268
|
+
if (!resolvedTo) {
|
|
269
|
+
return json({
|
|
270
|
+
error:
|
|
271
|
+
"action=add 时无法确定投递目标地址(当前 run 无 QQ 会话来源)。" +
|
|
272
|
+
'请显式传入 to(格式:qqbot:c2c:user_openid 或 qqbot:group:group_openid)后重试。',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// 账户 ID:会话路由优先,fallback 到 "default"
|
|
276
|
+
const resolvedAccountId = route?.accountId || "default";
|
|
267
277
|
if (!p.time) {
|
|
268
278
|
return json({ error: "action=add 时 time(时间)为必填参数。示例:\"5m\"、\"1h30m\"、\"0 8 * * *\"" });
|
|
269
279
|
}
|
|
@@ -299,7 +309,8 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
|
|
|
299
309
|
cronParams: onceJob,
|
|
300
310
|
summary: `⏰ ${formatDelay(delayMs)}后提醒: "${p.content}"`,
|
|
301
311
|
});
|
|
302
|
-
|
|
312
|
+
},
|
|
313
|
+
};
|
|
303
314
|
},
|
|
304
315
|
{ name: "qqbot_remind" },
|
|
305
316
|
);
|